├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── README.md ├── angular.json ├── config.xml ├── ionic.config.json ├── package-lock.json ├── package.json ├── resources │ ├── README.md │ ├── android │ │ ├── icon │ │ │ ├── drawable-hdpi-icon.png │ │ │ ├── drawable-ldpi-icon.png │ │ │ ├── drawable-mdpi-icon.png │ │ │ ├── drawable-xhdpi-icon.png │ │ │ ├── drawable-xxhdpi-icon.png │ │ │ └── drawable-xxxhdpi-icon.png │ │ ├── splash │ │ │ ├── drawable-land-hdpi-screen.png │ │ │ ├── drawable-land-ldpi-screen.png │ │ │ ├── drawable-land-mdpi-screen.png │ │ │ ├── drawable-land-xhdpi-screen.png │ │ │ ├── drawable-land-xxhdpi-screen.png │ │ │ ├── drawable-land-xxxhdpi-screen.png │ │ │ ├── drawable-port-hdpi-screen.png │ │ │ ├── drawable-port-ldpi-screen.png │ │ │ ├── drawable-port-mdpi-screen.png │ │ │ ├── drawable-port-xhdpi-screen.png │ │ │ ├── drawable-port-xxhdpi-screen.png │ │ │ └── drawable-port-xxxhdpi-screen.png │ │ └── xml │ │ │ └── network_security_config.xml │ ├── icon.png │ ├── ios │ │ ├── icon │ │ │ ├── icon-1024.png │ │ │ ├── icon-108@2x.png │ │ │ ├── icon-20.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-24@2x.png │ │ │ ├── icon-27.5@2x.png │ │ │ ├── icon-29.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-44@2x.png │ │ │ ├── icon-50.png │ │ │ ├── icon-50@2x.png │ │ │ ├── icon-60.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-72.png │ │ │ ├── icon-72@2x.png │ │ │ ├── icon-76.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-83.5@2x.png │ │ │ ├── icon-86@2x.png │ │ │ ├── icon-98@2x.png │ │ │ ├── icon-small.png │ │ │ ├── icon-small@2x.png │ │ │ ├── icon-small@3x.png │ │ │ ├── icon.png │ │ │ └── icon@2x.png │ │ └── splash │ │ │ ├── Default-1792h~iphone.png │ │ │ ├── Default-2436h.png │ │ │ ├── Default-2688h~iphone.png │ │ │ ├── Default-568h@2x~iphone.png │ │ │ ├── Default-667h.png │ │ │ ├── Default-736h.png │ │ │ ├── Default-Landscape-1792h~iphone.png │ │ │ ├── Default-Landscape-2436h.png │ │ │ ├── Default-Landscape-2688h~iphone.png │ │ │ ├── Default-Landscape-736h.png │ │ │ ├── Default-Landscape@2x~ipad.png │ │ │ ├── Default-Landscape@~ipadpro.png │ │ │ ├── Default-Landscape~ipad.png │ │ │ ├── Default-Portrait@2x~ipad.png │ │ │ ├── Default-Portrait@~ipadpro.png │ │ │ ├── Default-Portrait~ipad.png │ │ │ ├── Default@2x~iphone.png │ │ │ ├── Default@2x~universal~anyany.png │ │ │ └── Default~iphone.png │ └── splash.png ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── explore-container │ │ │ ├── explore-container.component.html │ │ │ ├── explore-container.component.scss │ │ │ ├── explore-container.component.spec.ts │ │ │ ├── explore-container.component.ts │ │ │ └── explore-container.module.ts │ │ ├── tab1 │ │ │ ├── tab1-routing.module.ts │ │ │ ├── tab1.module.ts │ │ │ ├── tab1.page.html │ │ │ ├── tab1.page.scss │ │ │ ├── tab1.page.spec.ts │ │ │ └── tab1.page.ts │ │ ├── tab2 │ │ │ ├── tab2-routing.module.ts │ │ │ ├── tab2.module.ts │ │ │ ├── tab2.page.html │ │ │ ├── tab2.page.scss │ │ │ ├── tab2.page.spec.ts │ │ │ └── tab2.page.ts │ │ ├── tab3 │ │ │ ├── tab3-routing.module.ts │ │ │ ├── tab3.module.ts │ │ │ ├── tab3.page.html │ │ │ ├── tab3.page.scss │ │ │ ├── tab3.page.spec.ts │ │ │ └── tab3.page.ts │ │ └── tabs │ │ │ ├── tabs-routing.module.ts │ │ │ ├── tabs.module.ts │ │ │ ├── tabs.page.html │ │ │ ├── tabs.page.scss │ │ │ ├── tabs.page.spec.ts │ │ │ └── tabs.page.ts │ ├── assets │ │ ├── icon │ │ │ └── favicon.png │ │ └── shapes.svg │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── global.scss │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── theme │ │ └── variables.scss │ └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── package-lock.json ├── package.json ├── plugin.xml ├── src ├── android │ ├── AckDatabase.java │ ├── FileTransferBackground.java │ ├── ProgressRequestBody.java │ ├── UploadEvent.java │ ├── UploadEventDao.java │ ├── UploadForegroundNotification.java │ ├── UploadNotification.java │ ├── UploadTask.java │ ├── config.gradle │ └── res │ │ └── ic_upload.png └── ios │ ├── FileTransferBackground.h │ ├── FileTransferBackground.m │ ├── FileUploader.h │ ├── FileUploader.m │ ├── UploadEvent.h │ └── UploadEvent.m ├── tests ├── package.json ├── plugin.xml ├── test-server │ ├── .gitignore │ ├── app.js │ ├── package-lock.json │ └── package.json ├── test-utils.js ├── tests.js ├── travis.sh ├── tree.jpg ├── tree2.jpg └── tree3.jpg └── www └── FileTransferManager.js /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | on: push 3 | jobs: 4 | publish: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: bahmutov/npm-install@v1 9 | - id: publish 10 | uses: JS-DevTools/npm-publish@v1 11 | with: 12 | token: ${{ secrets.npm_token }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #If ignorance is bliss, then somebody knock the smile off my face 2 | 3 | *.csproj.user 4 | *.suo 5 | *.cache 6 | Thumbs.db 7 | *.DS_Store 8 | 9 | *.bak 10 | *.cache 11 | *.log 12 | *.swp 13 | *.user 14 | *.vscode 15 | 16 | node_modules 17 | demo/node_modules 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coffeelint.json 2 | index.html 3 | src/demo* 4 | src/index* 5 | webpack.config.js 6 | dist/demo* 7 | .git* 8 | .npmignore 9 | node_modules 10 | demo/node_modules 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | matrix: 3 | include: 4 | - os: osx 5 | language: objective-c 6 | osx_image: xcode10.1 7 | cache: 8 | bundler: true 9 | cocoapods: true 10 | - os: linux 11 | jdk: oraclejdk8 12 | language: android 13 | android: 14 | components: 15 | - tools 16 | - platform-tools 17 | - tools 18 | - build-tools-28.0.3 19 | - android-22 20 | - android-28 21 | - sys-img-armeabi-v7a-android-22 22 | licenses: 23 | - 'android-sdk-preview-license-.+' 24 | - 'android-sdk-license-.+' 25 | - 'google-gdk-license-.+' 26 | script: 27 | - nvm use 28 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then rvm use 2.5.1 --install; fi 29 | - /bin/bash tests/travis.sh 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.1.2](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.1.1...4.1.2) (2024-11-05) 2 | * **android:** Return upload start and end time in upload response 3 | * **iOS:** Return upload start and end time in upload response 4 | 5 | ## [4.1.1](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.1.0...4.1.1) (2024-09-23) 6 | * **android:** Update cordova plugin file version to 8.1.0 7 | * **iOS:** Update cordova plugin file version to 8.1.0 8 | 9 | ## [4.1.0](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.10...4.1.0) (2024-04-30) 10 | * **android:** REVERTED Add Workers based on the number of parallelUploadsLimit and use a database to continue taking pending uploads in the Workers 11 | * **iOS:** Removed framework tag for pods 12 | 13 | ## [4.0.10](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.9...4.0.10) (2023-03-09) 14 | * **android:** Add Workers based on the number of parallelUploadsLimit and use a database to continue taking pending uploads in the Workers 15 | 16 | ## [4.0.9](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.8...4.0.9) (2023-02-21) 17 | * **android:** Upgrade WorkManager(2.8.0), Room(2.5.0) and Room Compiler(2.5.0) 18 | 19 | ## [4.0.8](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.7...4.0.8) (2023-01-20) 20 | * **android:** Add support for .db extension when assigning MediaType 21 | 22 | ## [4.0.7](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.6...4.0.7) (2022-11-15) 23 | * **android:** Compatibility with cordova-android v11 24 | 25 | ## [4.0.6](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.5...4.0.6) (2022-03-29) 26 | * **android:** Fix FilePath if there is a local webserver running on device 27 | * **ios:** Fix FilePath if there is a local webserver running on device 28 | 29 | ## [4.0.5](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.4...4.0.5) (2022-03-29) 30 | * **android:** Use multipart/form-data attribute as on iOS 31 | 32 | ## [4.0.4](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.3...4.0.4) (2022-03-29) 33 | * **android:** Add support for devices that does not support JSON as Content-Type 34 | 35 | ## [4.0.3](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.2...4.0.3) (2022-03-28) 36 | * **android:** Use cordova.getThreadPool() for execute Method 37 | 38 | ## [4.0.2](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.0...4.0.2) (2022-03-24) 39 | * **android:** Used ScheduledExecutorService and OutofQuotaPolicy added for Android 12 and above 40 | 41 | ## [4.0.1](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/4.0.0...4.0.1) (2022-03-04) 42 | * **android:** Fixed number of Threads for Executor Service and update Room 43 | 44 | ## [4.0.0](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/2.0.7...4.0.0) (2022-03-04) 45 | * **android:** Added WorkManager to handle uploads 46 | 47 | ## [2.0.5](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/2.0.4...2.0.5) (2021-07-07) 48 | * **android:** update demo project 49 | 50 | ## [2.0.4](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/2.0.3...2.0.4) (2021-06-18) 51 | ### Bug Fixes 52 | * **android:** Null pointer exception on manager service destroy 53 | 54 | 55 | ## [2.0.3](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/2.0.2...2.0.3) (2021-05-03) 56 | 57 | ## [3.0.2](https://github.com/spoonconsulting/cordova-plugin-background-upload/compare/3.0.1...3.0.2) (2021-04-22) 58 | ### Bug Fixes 59 | * **android:** Move gotev initialisation to a separate class that so it is initialised at app startup as per the android upload wiki (gotev) 60 | 61 | 62 | ## [2.0.1](https://github.com/spoonconsulting/cordova-plugin-background-upload/releases/tag/2.0.3) 63 | * **android:** Http Request method null for pendingUploads. setMethod(requestMethod) was null thus raising Java NullPointerException 64 | 65 | 66 | ## [3.0.1](https://github.com/spoonconsulting/cordova-plugin-background-upload) 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # cordova-plugin-background-upload 3 | This plugin provides a file upload functionality that can continue to run even while the app is in background. It includes progress updates suitable for long-term transfer operations of large files. 4 | 5 | [![npm version](https://badge.fury.io/js/@spoonconsulting%2Fcordova-plugin-background-upload.svg)](https://badge.fury.io/js/@spoonconsulting%2Fcordova-plugin-background-upload) 6 | [![Build Status](https://travis-ci.com/spoonconsulting/cordova-plugin-background-upload.svg?branch=master)](https://travis-ci.org/spoonconsulting/cordova-plugin-background-upload) 7 | 8 | **Supported Platforms** 9 | - iOS 10 | - Android 11 | 12 | 13 | **Installation** 14 | 15 | To install the plugin: 16 | 17 | ``` 18 | cordova plugin add @spoonconsulting/cordova-plugin-background-upload --save 19 | ``` 20 | 21 | To uninstall this plugin: 22 | ``` 23 | cordova plugin rm @spoonconsulting/cordova-plugin-background-upload 24 | ``` 25 | 26 | **Sample usage** 27 | 28 | The plugin needs to be initialised before any upload. Ideally this should be called on application start. The uploader will provide global events which can be used to check the progress of the uploads. By default, the maximum number of parallel uploads allowed is set to 1. You can override it by changing the configuration on init. 29 | ```javascript 30 | declare var FileTransferManager: any; 31 | var config = {}; 32 | var uploader = FileTransferManager.init(config, callback); 33 | ``` 34 | 35 | **Methods** 36 | 37 | ### uploader.init(config, callback) 38 | Initialises the uploader with provided configuration. To control the number of parallel uploads, pass `parallelUploadsLimit` in config. 39 | The callback is used to track progress of the uploads 40 | `var uploader = FileTransferManager.init({parallelUploadsLimit: 2}, event => {});` 41 | 42 | ### uploader.startUpload(payload) 43 | Adds an upload. In case the plugin was not able to enqueue the upload, an error will be emitted in the global event listener. 44 | ```javascript 45 | var payload = { 46 | id: "c3a4b4c7-4f1e-4c69-a951-773602e269fb", 47 | filePath: "/storage/emulated/0/Download/Heli.divx", 48 | fileKey: "file", 49 | serverUrl: "http://requestb.in/14cizzj1", 50 | notificationTitle: "Uploading images", 51 | headers: { 52 | api_key: "asdasdwere123sad" 53 | }, 54 | parameters: { 55 | signature: "a_signature_hash", 56 | timestamp: 112321321 57 | } 58 | }; 59 | uploader.startUpload(payload); 60 | ``` 61 | Param | Description 62 | -------- | ------- 63 | id | a unique id of the file (UUID string) 64 | filePath | the absolute path for the file to upload 65 | fileKey | the name of the key to use for the file 66 | serverUrl | remote server url 67 | headers | custom http headers 68 | parameters | custom parameters for multipart data 69 | notificationTitle | Notification title when file is being uploaded (Android only) 70 | 71 | 72 | ### uploader.removeUpload(uploadId, successCallback, errorCallback) 73 | Cancels and removes an upload 74 | ```javascript 75 | uploader.removeUpload(uploadId, function () { 76 | //upload aborted 77 | }, function (err) { 78 | //could not abort the upload 79 | }); 80 | ``` 81 | 82 | 83 | ### uploader.acknowledgeEvent(eventId) 84 | Confirms event received and remove it from plugin cache 85 | ```javascript 86 | uploader.acknowledgeEvent(eventId); 87 | ``` 88 | 89 | 90 | The uploader will provide global events which can be used to check the status of the uploads. 91 | ```javascript 92 | FileTransferManager.init({}, function (event) { 93 | if (event.state == 'UPLOADED') { 94 | console.log("upload: " + event.id + " has been completed successfully"); 95 | console.log(event.statusCode, event.serverResponse); 96 | } else if (event.state == 'FAILED') { 97 | if (event.id) { 98 | console.log("upload: " + event.id + " has failed"); 99 | } else { 100 | console.error("uploader caught an error: " + event.error); 101 | } 102 | } else if (event.state == 'UPLOADING') { 103 | console.log("uploading: " + event.id + " progress: " + event.progress + "%"); 104 | } 105 | }); 106 | 107 | ``` 108 | 109 | To prevent any event loss while transitioning between native and Javascript side, the plugin stores success/failure events on disk. Once you have received the event, you will need to acknowledge it else it will be broadcast again when the plugin is initialised. Progress events do not have eventId and are not persisted. 110 | ```javascript 111 | if (event.eventId) { 112 | uploader.acknowledgeEvent(event.eventId, function(){ 113 | //success 114 | }, function (error){ 115 | //error 116 | }); 117 | } 118 | ``` 119 | An event has the following attributes: 120 | 121 | Property | Comment 122 | -------- | ------- 123 | id | id of the upload 124 | state | state of the upload (either `UPLOADING`, `UPLOADED` or `FAILED`) 125 | statusCode | response code returned by server after upload is completed 126 | serverResponse | server response received after upload is completed 127 | error | error message in case of failure 128 | errorCode | error code for any exception encountered 129 | progress | progress for ongoing upload 130 | eventId | id of the event 131 | 132 | 133 | ## iOS 134 | The plugin runs on ios 10.0 and above and internally uses [AFNetworking](https://github.com/AFNetworking/AFNetworking). AFNetworking uses [NSURLSession](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html#//apple_ref/doc/uid/TP40013509-SW44) under the hood to perform the upload in a background session. When an upload is initiated, it will continue until it has been completed successfully or until the user kills the application. If the application is terminated by the OS, the uploads will still continue. When the user relaunches the application, after calling the init method, events will be emitted with the ids of these uploads. If the user kills the application by swiping it up from the multitasking pane, the uploads will not be continued. Upload tasks in background sessions are automatically retried by the URL loading system after network errors as decided by the OS. 135 | 136 | ## Android 137 | The minimum API level required is 21(Android 5) and the background file upload is handled by the WorkMananger library. If you have configured a notification to appear in the notifications area, the uploads will continue even if the user kills the app manually. If an upload is added when there is no network connection, it will be retried as soon as the network becomes reachable unless the app has already been killed. 138 | 139 | On Android 12 and above, there are strict limitations on background services that does not allow to start a Foreground Service when the app is processing in background(https://developer.android.com/guide/components/foreground-services). Hence to prevent this on Android 12 and above, we have a classic notification. On Android 11 and below, we are still using foreground service along with WorkManager to start the notification. 140 | 141 | ## Migration notes for v2.0 142 | - When v2 of the plugin is launched on an app containing uploads still in progress from v1 version, it will mark all of them as `FAILED` with `errorCode` 500 so that they can be retried. 143 | - If an upload is cancelled, an event with status `FAILED` and error code `-999` will be broadcasted in the global callback. It is up to the application to properly handle cancelled callbacks. 144 | - v2 removes the events `success`, `error`, `progress` and instead uses a single callback for all events delivery: 145 | ```javascript 146 | uploader.on('event', function (event) { 147 | //use event.state to handle different scenarios 148 | }); 149 | ``` 150 | - Events need to be acknowledged to be removed. Failure to do so will result in all saved events being broadcast on `init`. 151 | -`showNotification` parameter has been removed (A notification will always be shown on Android during upload) 152 | 153 | 154 | ## README for v1.0 155 | The README for the previous version can be found [here](https://github.com/spoonconsulting/cordova-plugin-background-upload/blob/eacce4385ae497188307a9944c2f353571a463a2/README.md). 156 | 157 | ## License 158 | cordova-plugin-background-upload is licensed under the Apache v2 License. 159 | 160 | ## Credits 161 | cordova-plugin-background-upload is brought to you by [Spoon Consulting Ltd] (http://www.spoonconsulting.com/). 162 | -------------------------------------------------------------------------------- /demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /demo/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/ng-cli-compat", 13 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 14 | "plugin:@angular-eslint/template/process-inline-templates" 15 | ], 16 | "rules": { 17 | "@angular-eslint/component-class-suffix": [ 18 | "error", 19 | { 20 | "suffixes": ["Page", "Component"] 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "app", 28 | "style": "kebab-case" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "type": "attribute", 35 | "prefix": "app", 36 | "style": "camelCase" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": ["*.html"], 43 | "extends": ["plugin:@angular-eslint/template/recommended"], 44 | "rules": {} 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | .tmp 7 | *.tmp 8 | *.tmp.* 9 | *.sublime-project 10 | *.sublime-workspace 11 | .DS_Store 12 | Thumbs.db 13 | UserInterfaceState.xcuserstate 14 | $RECYCLE.BIN/ 15 | 16 | *.log 17 | log.txt 18 | npm-debug.log* 19 | 20 | /.idea 21 | /.angular 22 | /.ionic 23 | /.sass-cache 24 | /.sourcemaps 25 | /.versions 26 | /.vscode 27 | /coverage 28 | /dist 29 | /node_modules 30 | /platforms 31 | /plugins 32 | /www 33 | -------------------------------------------------------------------------------- /demo/.npmignore: -------------------------------------------------------------------------------- 1 | ./demo 2 | ./demo-new 3 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Upload Demo 2 | 3 | Upload Demo is a sample using cordova-plugin-background-upload plugin 4 | 5 | ## Getting started 6 | 7 | Install dependencies 8 | ```bash 9 | npm i 10 | ``` 11 | 12 | Add platform and run on Android 13 | ```bash 14 | ionic cordova platform add android 15 | ionic cordova run android 16 | ``` 17 | Add platform and run on iOS 18 | 19 | ```bash 20 | ionic cordova platform add ios 21 | ionic cordova run ios 22 | ``` 23 | 24 | ## License 25 | 26 | Upload Demo is licensed under the Apache v2 License. 27 | -------------------------------------------------------------------------------- /demo/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "newProjectRoot": "projects", 6 | "projects": { 7 | "app": { 8 | "root": "", 9 | "sourceRoot": "src", 10 | "projectType": "application", 11 | "prefix": "app", 12 | "schematics": {}, 13 | "architect": { 14 | "build": { 15 | "builder": "@angular-devkit/build-angular:browser", 16 | "options": { 17 | "outputPath": "www", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "src/assets", 26 | "output": "assets" 27 | }, 28 | { 29 | "glob": "**/*.svg", 30 | "input": "node_modules/ionicons/dist/ionicons/svg", 31 | "output": "./svg" 32 | } 33 | ], 34 | "styles": ["src/theme/variables.scss", "src/global.scss"], 35 | "scripts": [], 36 | "aot": false, 37 | "vendorChunk": true, 38 | "extractLicenses": false, 39 | "buildOptimizer": false, 40 | "sourceMap": true, 41 | "optimization": false, 42 | "namedChunks": true 43 | }, 44 | "configurations": { 45 | "production": { 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "optimization": true, 53 | "outputHashing": "all", 54 | "sourceMap": false, 55 | "namedChunks": false, 56 | "aot": true, 57 | "extractLicenses": true, 58 | "vendorChunk": false, 59 | "buildOptimizer": true, 60 | "budgets": [ 61 | { 62 | "type": "initial", 63 | "maximumWarning": "2mb", 64 | "maximumError": "5mb" 65 | } 66 | ] 67 | }, 68 | "ci": { 69 | "progress": false 70 | } 71 | } 72 | }, 73 | "serve": { 74 | "builder": "@angular-devkit/build-angular:dev-server", 75 | "options": { 76 | "browserTarget": "app:build" 77 | }, 78 | "configurations": { 79 | "production": { 80 | "browserTarget": "app:build:production" 81 | }, 82 | "ci": { 83 | "progress": false 84 | } 85 | } 86 | }, 87 | "extract-i18n": { 88 | "builder": "@angular-devkit/build-angular:extract-i18n", 89 | "options": { 90 | "browserTarget": "app:build" 91 | } 92 | }, 93 | "lint": { 94 | "builder": "@angular-eslint/builder:lint", 95 | "options": { 96 | "lintFilePatterns": [ 97 | "src/**/*.ts", 98 | "src/**/*.html" 99 | ] 100 | } 101 | }, 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "e2e/protractor.conf.js", 106 | "devServerTarget": "app:serve" 107 | }, 108 | "configurations": { 109 | "production": { 110 | "devServerTarget": "app:serve:production" 111 | }, 112 | "ci": { 113 | "devServerTarget": "app:serve:ci" 114 | } 115 | } 116 | }, 117 | "ionic-cordova-build": { 118 | "builder": "@ionic/cordova-builders:cordova-build", 119 | "options": { 120 | "browserTarget": "app:build" 121 | }, 122 | "configurations": { 123 | "production": { 124 | "browserTarget": "app:build:production" 125 | } 126 | } 127 | }, 128 | "ionic-cordova-serve": { 129 | "builder": "@ionic/cordova-builders:cordova-serve", 130 | "options": { 131 | "cordovaBuildTarget": "app:ionic-cordova-build", 132 | "devServerTarget": "app:serve" 133 | }, 134 | "configurations": { 135 | "production": { 136 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 137 | "devServerTarget": "app:serve:production" 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "cli": { 145 | "analytics": false, 146 | "defaultCollection": "@ionic/angular-toolkit" 147 | }, 148 | "schematics": { 149 | "@ionic/angular-toolkit:component": { 150 | "styleext": "scss" 151 | }, 152 | "@ionic/angular-toolkit:page": { 153 | "styleext": "scss" 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /demo/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Upload Demo 4 | An awesome Ionic/Cordova app. 5 | Ionic Framework Team 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | -------------------------------------------------------------------------------- /demo/ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-new", 3 | "integrations": { 4 | "cordova": {} 5 | }, 6 | "type": "angular" 7 | } 8 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.2", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "~14.2.2", 17 | "@angular/core": "~14.2.2", 18 | "@angular/forms": "~14.2.2", 19 | "@angular/platform-browser": "~14.2.2", 20 | "@angular/platform-browser-dynamic": "~14.2.2", 21 | "@angular/router": "~14.2.2", 22 | "@awesome-cordova-plugins/background-upload": "^5.45.0", 23 | "@awesome-cordova-plugins/core": "^5.45.0", 24 | "@awesome-cordova-plugins/file": "^5.45.0", 25 | "@awesome-cordova-plugins/image-picker": "^5.45.0", 26 | "@ionic/angular": "^6.2.5", 27 | "cordova-ios": "6.2.0", 28 | "rxjs": "~7.5.6", 29 | "tslib": "^2.4.0", 30 | "zone.js": "~0.11.8" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~14.2.3", 34 | "@angular-eslint/builder": "~14.0.4", 35 | "@angular-eslint/eslint-plugin": "~14.0.4", 36 | "@angular-eslint/eslint-plugin-template": "~14.0.4", 37 | "@angular-eslint/template-parser": "~14.0.4", 38 | "@angular/cli": "~14.2.3", 39 | "@angular/compiler": "~14.2.2", 40 | "@angular/compiler-cli": "~14.2.2", 41 | "@angular/language-service": "~14.2.2", 42 | "@ionic/angular-toolkit": "^7.0.0", 43 | "@ionic/cordova-builders": "^7.0.0", 44 | "@spoonconsulting/cordova-plugin-background-upload": "git+https://github.com/spoonconsulting/cordova-plugin-background-upload.git#master", 45 | "@spoonconsulting/cordova-plugin-telerik-imagepicker": "^2.0.1", 46 | "@types/node": "^12.11.1", 47 | "@typescript-eslint/eslint-plugin": "5.37.0", 48 | "@typescript-eslint/parser": "5.37.0", 49 | "ajv": "8.8.2", 50 | "cordova-android": "^11.0.0", 51 | "cordova-plugin-device": "2.1.0", 52 | "cordova-plugin-file": "8.1.0", 53 | "cordova-plugin-ionic-keyboard": "^2.2.0", 54 | "cordova-plugin-ionic-webview": "^5.0.0", 55 | "cordova-plugin-statusbar": "3.0.0", 56 | "cordova-res": "^0.15.4", 57 | "eslint": "^8.23.1", 58 | "eslint-plugin-import": "2.26.0", 59 | "eslint-plugin-jsdoc": "39.3.6", 60 | "eslint-plugin-prefer-arrow": "1.2.3", 61 | "native-run": "^1.7.0", 62 | "ts-node": "~10.9.1", 63 | "typescript": "~4.8.2" 64 | }, 65 | "description": "An Ionic project", 66 | "cordova": { 67 | "plugins": { 68 | "cordova-plugin-statusbar": {}, 69 | "cordova-plugin-device": {}, 70 | "cordova-plugin-ionic-webview": { 71 | "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+" 72 | }, 73 | "cordova-plugin-ionic-keyboard": {}, 74 | "cordova-plugin-file": { 75 | "ANDROIDX_WEBKIT_VERSION": "1.4.0" 76 | }, 77 | "@spoonconsulting/cordova-plugin-telerik-imagepicker": {}, 78 | "@spoonconsulting/cordova-plugin-background-upload": {} 79 | }, 80 | "platforms": [ 81 | "android", 82 | "ios" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /demo/resources/README.md: -------------------------------------------------------------------------------- 1 | These are Cordova resources. You can replace icon.png and splash.png and run 2 | `ionic cordova resources` to generate custom icons and splash screens for your 3 | app. See `ionic cordova resources --help` for details. 4 | 5 | Cordova reference documentation: 6 | 7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html 8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/ 9 | -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /demo/resources/android/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | localhost 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/icon.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-1024.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-108@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-108@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-20.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-20@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-20@3x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-24@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-27.5@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-29.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-29@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-29@3x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-44@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-86@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-98@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /demo/resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-1792h~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-1792h~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-2436h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-2436h.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-2688h~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-2688h~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape-1792h~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape-1792h~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape-2436h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape-2436h.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape-2688h~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape-2688h~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape@~ipadpro.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Portrait@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Portrait@~ipadpro.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /demo/resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /demo/resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/resources/splash.png -------------------------------------------------------------------------------- /demo/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule) 8 | } 9 | ]; 10 | @NgModule({ 11 | imports: [ 12 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) 13 | ], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/src/app/app.component.scss -------------------------------------------------------------------------------- /demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | 8 | beforeEach(waitForAsync(() => { 9 | 10 | TestBed.configureTestingModule({ 11 | declarations: [AppComponent], 12 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 13 | }).compileComponents(); 14 | })); 15 | 16 | it('should create the app', () => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | }); 21 | // TODO: add more tests! 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: 'app.component.html', 8 | styleUrls: ['app.component.scss'], 9 | }) 10 | export class AppComponent { 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | 7 | import { AppRoutingModule } from './app-routing.module'; 8 | import { AppComponent } from './app.component'; 9 | 10 | @NgModule({ 11 | declarations: [AppComponent], 12 | entryComponents: [], 13 | imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], 14 | providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], 15 | bootstrap: [AppComponent], 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /demo/src/app/explore-container/explore-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ name }} 3 |

Explore UI Components

4 |
-------------------------------------------------------------------------------- /demo/src/app/explore-container/explore-container.component.scss: -------------------------------------------------------------------------------- 1 | #container { 2 | text-align: center; 3 | 4 | position: absolute; 5 | left: 0; 6 | right: 0; 7 | top: 50%; 8 | transform: translateY(-50%); 9 | } 10 | 11 | #container strong { 12 | font-size: 20px; 13 | line-height: 26px; 14 | } 15 | 16 | #container p { 17 | font-size: 16px; 18 | line-height: 22px; 19 | 20 | color: #8c8c8c; 21 | 22 | margin: 0; 23 | } 24 | 25 | #container a { 26 | text-decoration: none; 27 | } -------------------------------------------------------------------------------- /demo/src/app/explore-container/explore-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { ExploreContainerComponent } from './explore-container.component'; 5 | 6 | describe('ExploreContainerComponent', () => { 7 | let component: ExploreContainerComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ ExploreContainerComponent ], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(ExploreContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /demo/src/app/explore-container/explore-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-explore-container', 5 | templateUrl: './explore-container.component.html', 6 | styleUrls: ['./explore-container.component.scss'], 7 | }) 8 | export class ExploreContainerComponent implements OnInit { 9 | @Input() name: string; 10 | 11 | constructor() { } 12 | 13 | ngOnInit() {} 14 | 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/app/explore-container/explore-container.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { IonicModule } from '@ionic/angular'; 6 | 7 | import { ExploreContainerComponent } from './explore-container.component'; 8 | 9 | @NgModule({ 10 | imports: [ CommonModule, FormsModule, IonicModule], 11 | declarations: [ExploreContainerComponent], 12 | exports: [ExploreContainerComponent] 13 | }) 14 | export class ExploreContainerComponentModule {} 15 | -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { Tab1Page } from './tab1.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: Tab1Page, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class Tab1PageRoutingModule {} 17 | -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { Tab1Page } from './tab1.page'; 6 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 7 | 8 | import { Tab1PageRoutingModule } from './tab1-routing.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | ExploreContainerComponentModule, 16 | Tab1PageRoutingModule 17 | ], 18 | declarations: [Tab1Page], 19 | }) 20 | export class Tab1PageModule {} 21 | -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tab 1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Tab 1 13 | 14 | 15 | 16 | 17 | Add images 18 | 19 | 20 | 24 | 25 | Upload images 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/src/app/tab1/tab1.page.scss -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 4 | 5 | import { Tab1Page } from './tab1.page'; 6 | 7 | describe('Tab1Page', () => { 8 | let component: Tab1Page; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [Tab1Page], 14 | imports: [IonicModule.forRoot(), ExploreContainerComponentModule] 15 | }).compileComponents(); 16 | 17 | fixture = TestBed.createComponent(Tab1Page); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | })); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo/src/app/tab1/tab1.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgZone } from '@angular/core'; 2 | import { AlertController, Platform } from '@ionic/angular'; 3 | import { ImagePicker } from '@awesome-cordova-plugins/image-picker/ngx'; 4 | import { File } from '@awesome-cordova-plugins/file/ngx'; 5 | import { FileTransferManager, BackgroundUpload } from '@awesome-cordova-plugins/background-upload/ngx'; 6 | 7 | const TEST_UPLOAD_URL = 'https://api.2ip.me/api-speedtest/en/dev.null'; 8 | const ID_OFFSET = 100; 9 | 10 | @Component({ 11 | selector: 'app-tab1', 12 | templateUrl: 'tab1.page.html', 13 | styleUrls: ['tab1.page.scss'], 14 | providers: [ImagePicker, File, BackgroundUpload], 15 | }) 16 | export class Tab1Page { 17 | uploader: FileTransferManager; 18 | 19 | images: Map = new Map(); 20 | imageUris: Map = new Map(); 21 | uploadStates: Map = new Map(); 22 | 23 | private isCordova = this.platform.is('cordova'); 24 | 25 | constructor( 26 | private platform: Platform, 27 | private zone: NgZone, 28 | private alertController: AlertController, 29 | private imagePicker: ImagePicker, 30 | private file: File, 31 | private backgroundUpload: BackgroundUpload 32 | ) { 33 | this.platform.ready().then(() => { 34 | 35 | this.initUpload(); 36 | 37 | }); 38 | } 39 | 40 | async onPickImage() { 41 | if (!this.isCordova) { 42 | return 43 | } 44 | // Check permissions beforehand because if we let imagePicker do it 45 | // he will return nonsense 46 | const hasPermissions = await this.imagePicker.hasReadPermission(); 47 | if (!hasPermissions) { 48 | await this.imagePicker.requestReadPermission(); 49 | return; 50 | } 51 | 52 | const options = { 53 | maximumImagesCount: 100 54 | }; 55 | 56 | try { 57 | const uris: Array = await this.imagePicker.getPictures(options); 58 | const generatedKeys = this.generateUniqueIds(uris.length); 59 | console.log(uris); 60 | uris.forEach((uri, i) => { 61 | const pathSplit = uri.split('/'); 62 | const dir = 'file://' + pathSplit.join('/'); 63 | this.imageUris.set(generatedKeys[i], dir); 64 | }); 65 | 66 | const data = await Promise.all(uris.map((uri) => { 67 | const pathSplit = uri.split('/'); 68 | const filename = pathSplit.pop(); 69 | const dir = 'file://' + pathSplit.join('/'); 70 | return this.file.readAsDataURL(dir, filename); 71 | })); 72 | 73 | data.forEach((d, i) => { 74 | this.images.set(generatedKeys[i], d); 75 | }); 76 | } catch (err) { 77 | const alert = await this.alertController.create({ 78 | header: 'An error occurred', 79 | message: JSON.stringify(err), 80 | buttons: ['Ok'], 81 | }); 82 | 83 | await alert.present(); 84 | } 85 | } 86 | 87 | async onClickImage(id: number) { 88 | if (!this.uploadStates.has(id)) { 89 | // Start upload 90 | this.uploadImage(id); 91 | } else { 92 | // Remove download 93 | await this.removeImage(id); 94 | } 95 | } 96 | 97 | async onTapUploadButton() { 98 | for (const [key, value] of this.images) { 99 | if (!this.uploadStates.has(key)) { 100 | // Start upload 101 | this.uploadImage(key); 102 | } else { 103 | // Remove download 104 | await this.removeImage(key); 105 | } 106 | } 107 | } 108 | 109 | private initUpload(){ 110 | if(!this.isCordova){ 111 | return; 112 | } 113 | if(this.uploader){ 114 | return; 115 | } 116 | this.uploader = this.backgroundUpload.init({ 117 | callBack: event => { 118 | console.log(event); 119 | this.zone.run(() => { 120 | const id = Number.parseInt(event.id, 10); 121 | 122 | if (!this.uploadStates.has(id)) { 123 | this.uploadStates.set(id, new UploadState()); 124 | } 125 | const state = this.uploadStates.get(id); 126 | 127 | switch (event.state) { 128 | case 'UPLOADING': 129 | state.status = UploadStatus.InProgress; 130 | state.progress = event.progress / 100.0; 131 | break; 132 | 133 | case 'UPLOADED': 134 | state.status = UploadStatus.Done; 135 | state.progress = 1.0; 136 | break; 137 | 138 | case 'FAILED': 139 | state.status = UploadStatus.Failed; 140 | this.alertController.create({ 141 | header: 'Upload failed', 142 | message: event.error, 143 | buttons: ['Ok'], 144 | }) 145 | .then((alert) => alert.present()); 146 | break; 147 | } 148 | 149 | console.log('New state:', state); 150 | 151 | if (event.eventId) { 152 | console.log('ACK'); 153 | this.uploader.acknowledgeEvent(event.eventId); 154 | } 155 | }); 156 | } 157 | }) 158 | } 159 | 160 | private uploadImage(id: number) { 161 | for (let i = 0; i < 10; i++) { 162 | const uri = this.imageUris.get(id); 163 | console.log('Upload id', id); 164 | 165 | const options = { 166 | serverUrl: TEST_UPLOAD_URL, 167 | filePath: uri, 168 | fileKey: 'file', 169 | id: String(id), 170 | notificationTitle: 'Uploading image' 171 | }; 172 | this.uploader.startUpload(options); 173 | console.log('Upload submitted'); 174 | } 175 | } 176 | 177 | private async removeImage(id: number) { 178 | const state = this.uploadStates.get(id); 179 | const res = await this.uploader.removeUpload(id); 180 | if (res) { 181 | console.log('Remove result:', res); 182 | this.zone.run(() => { 183 | state.status = UploadStatus.Aborted; 184 | state.progress = 1.0; 185 | }); 186 | } else { 187 | console.warn('Remove error:', res); 188 | const alert = await this.alertController.create({ 189 | header: 'Error removing upload', 190 | }); 191 | await alert.present(); 192 | } 193 | } 194 | 195 | private generateUniqueIds(count: number): Array { 196 | const random = () => Math.round(Math.random() * 10000); 197 | const keys = Array(count).fill(undefined); 198 | 199 | for (let i = 0; i < count; i++) { 200 | let key = random(); 201 | while (this.imageUris.has(key) || keys.includes(key) || key === 0) { 202 | key = random(); 203 | } 204 | keys[i] = key; 205 | } 206 | 207 | return keys; 208 | } 209 | } 210 | 211 | export enum UploadStatus { 212 | // eslint-disable-next-line @typescript-eslint/naming-convention 213 | InProgress, 214 | // eslint-disable-next-line @typescript-eslint/naming-convention 215 | Done, 216 | // eslint-disable-next-line @typescript-eslint/naming-convention 217 | Failed, 218 | // eslint-disable-next-line @typescript-eslint/naming-convention 219 | Aborted, 220 | } 221 | 222 | export class UploadState { 223 | status = UploadStatus.InProgress; 224 | progress = 0.0; 225 | 226 | get color(): string { 227 | switch (this.status) { 228 | case UploadStatus.InProgress: 229 | return 'tertiary'; 230 | case UploadStatus.Done: 231 | return 'success'; 232 | case UploadStatus.Failed: 233 | return 'danger'; 234 | case UploadStatus.Aborted: 235 | return 'dark'; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { Tab2Page } from './tab2.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: Tab2Page, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class Tab2PageRoutingModule {} 17 | -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { Tab2Page } from './tab2.page'; 7 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 8 | 9 | import { Tab2PageRoutingModule } from './tab2-routing.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | IonicModule, 14 | CommonModule, 15 | FormsModule, 16 | ExploreContainerComponentModule, 17 | Tab2PageRoutingModule 18 | ], 19 | declarations: [Tab2Page] 20 | }) 21 | export class Tab2PageModule {} 22 | -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tab 2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Tab 2 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/src/app/tab2/tab2.page.scss -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 4 | 5 | import { Tab2Page } from './tab2.page'; 6 | 7 | describe('Tab2Page', () => { 8 | let component: Tab2Page; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [Tab2Page], 14 | imports: [IonicModule.forRoot(), ExploreContainerComponentModule] 15 | }).compileComponents(); 16 | 17 | fixture = TestBed.createComponent(Tab2Page); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | })); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo/src/app/tab2/tab2.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tab2', 5 | templateUrl: 'tab2.page.html', 6 | styleUrls: ['tab2.page.scss'] 7 | }) 8 | export class Tab2Page { 9 | 10 | constructor() {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { Tab3Page } from './tab3.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: Tab3Page, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class Tab3PageRoutingModule {} 17 | -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { Tab3Page } from './tab3.page'; 7 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 8 | 9 | import { Tab3PageRoutingModule } from './tab3-routing.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | IonicModule, 14 | CommonModule, 15 | FormsModule, 16 | ExploreContainerComponentModule, 17 | RouterModule.forChild([{ path: '', component: Tab3Page }]), 18 | Tab3PageRoutingModule, 19 | ], 20 | declarations: [Tab3Page] 21 | }) 22 | export class Tab3PageModule {} 23 | -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tab 3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Tab 3 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/src/app/tab3/tab3.page.scss -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; 4 | 5 | import { Tab3Page } from './tab3.page'; 6 | 7 | describe('Tab3Page', () => { 8 | let component: Tab3Page; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [Tab3Page], 14 | imports: [IonicModule.forRoot(), ExploreContainerComponentModule] 15 | }).compileComponents(); 16 | 17 | fixture = TestBed.createComponent(Tab3Page); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | })); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo/src/app/tab3/tab3.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tab3', 5 | templateUrl: 'tab3.page.html', 6 | styleUrls: ['tab3.page.scss'] 7 | }) 8 | export class Tab3Page { 9 | 10 | constructor() {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { TabsPage } from './tabs.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'tabs', 8 | component: TabsPage, 9 | children: [ 10 | { 11 | path: 'tab1', 12 | loadChildren: () => import('../tab1/tab1.module').then(m => m.Tab1PageModule) 13 | }, 14 | { 15 | path: 'tab2', 16 | loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule) 17 | }, 18 | { 19 | path: 'tab3', 20 | loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule) 21 | }, 22 | { 23 | path: '', 24 | redirectTo: '/tabs/tab1', 25 | pathMatch: 'full' 26 | } 27 | ] 28 | }, 29 | { 30 | path: '', 31 | redirectTo: '/tabs/tab1', 32 | pathMatch: 'full' 33 | } 34 | ]; 35 | 36 | @NgModule({ 37 | imports: [RouterModule.forChild(routes)], 38 | }) 39 | export class TabsPageRoutingModule {} 40 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { TabsPageRoutingModule } from './tabs-routing.module'; 7 | 8 | import { TabsPage } from './tabs.page'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | TabsPageRoutingModule 16 | ], 17 | declarations: [TabsPage] 18 | }) 19 | export class TabsPageModule {} 20 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tab 1 7 | 8 | 9 | 10 | 11 | Tab 2 12 | 13 | 14 | 15 | 16 | Tab 3 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { TabsPage } from './tabs.page'; 5 | 6 | describe('TabsPage', () => { 7 | let component: TabsPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [TabsPage], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(TabsPage); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /demo/src/app/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tabs', 5 | templateUrl: 'tabs.page.html', 6 | styleUrls: ['tabs.page.scss'] 7 | }) 8 | export class TabsPage { 9 | 10 | constructor() {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/demo/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /demo/src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /demo/src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import "~@ionic/angular/css/core.css"; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import "~@ionic/angular/css/normalize.css"; 17 | @import "~@ionic/angular/css/structure.css"; 18 | @import "~@ionic/angular/css/typography.css"; 19 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import "~@ionic/angular/css/padding.css"; 23 | @import "~@ionic/angular/css/float-elements.css"; 24 | @import "~@ionic/angular/css/text-alignment.css"; 25 | @import "~@ionic/angular/css/text-transformation.css"; 26 | @import "~@ionic/angular/css/flex-utils.css"; 27 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /demo/src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #3dc2ff; 16 | --ion-color-secondary-rgb: 61, 194, 255; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #36abe0; 20 | --ion-color-secondary-tint: #50c8ff; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #5260ff; 24 | --ion-color-tertiary-rgb: 82, 96, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #4854e0; 28 | --ion-color-tertiary-tint: #6370ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #2dd36f; 32 | --ion-color-success-rgb: 45, 211, 111; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #28ba62; 36 | --ion-color-success-tint: #42d77d; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffc409; 40 | --ion-color-warning-rgb: 255, 196, 9; 41 | --ion-color-warning-contrast: #000000; 42 | --ion-color-warning-contrast-rgb: 0, 0, 0; 43 | --ion-color-warning-shade: #e0ac08; 44 | --ion-color-warning-tint: #ffca22; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #eb445a; 48 | --ion-color-danger-rgb: 235, 68, 90; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #cf3c4f; 52 | --ion-color-danger-tint: #ed576b; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 36, 40; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #92949c; 64 | --ion-color-medium-rgb: 146, 148, 156; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #808289; 68 | --ion-color-medium-tint: #9d9fa6; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 245, 248; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | /* 81 | * Dark Colors 82 | * ------------------------------------------- 83 | */ 84 | 85 | body { 86 | --ion-color-primary: #428cff; 87 | --ion-color-primary-rgb: 66,140,255; 88 | --ion-color-primary-contrast: #ffffff; 89 | --ion-color-primary-contrast-rgb: 255,255,255; 90 | --ion-color-primary-shade: #3a7be0; 91 | --ion-color-primary-tint: #5598ff; 92 | 93 | --ion-color-secondary: #50c8ff; 94 | --ion-color-secondary-rgb: 80,200,255; 95 | --ion-color-secondary-contrast: #ffffff; 96 | --ion-color-secondary-contrast-rgb: 255,255,255; 97 | --ion-color-secondary-shade: #46b0e0; 98 | --ion-color-secondary-tint: #62ceff; 99 | 100 | --ion-color-tertiary: #6a64ff; 101 | --ion-color-tertiary-rgb: 106,100,255; 102 | --ion-color-tertiary-contrast: #ffffff; 103 | --ion-color-tertiary-contrast-rgb: 255,255,255; 104 | --ion-color-tertiary-shade: #5d58e0; 105 | --ion-color-tertiary-tint: #7974ff; 106 | 107 | --ion-color-success: #2fdf75; 108 | --ion-color-success-rgb: 47,223,117; 109 | --ion-color-success-contrast: #000000; 110 | --ion-color-success-contrast-rgb: 0,0,0; 111 | --ion-color-success-shade: #29c467; 112 | --ion-color-success-tint: #44e283; 113 | 114 | --ion-color-warning: #ffd534; 115 | --ion-color-warning-rgb: 255,213,52; 116 | --ion-color-warning-contrast: #000000; 117 | --ion-color-warning-contrast-rgb: 0,0,0; 118 | --ion-color-warning-shade: #e0bb2e; 119 | --ion-color-warning-tint: #ffd948; 120 | 121 | --ion-color-danger: #ff4961; 122 | --ion-color-danger-rgb: 255,73,97; 123 | --ion-color-danger-contrast: #ffffff; 124 | --ion-color-danger-contrast-rgb: 255,255,255; 125 | --ion-color-danger-shade: #e04055; 126 | --ion-color-danger-tint: #ff5b71; 127 | 128 | --ion-color-dark: #f4f5f8; 129 | --ion-color-dark-rgb: 244,245,248; 130 | --ion-color-dark-contrast: #000000; 131 | --ion-color-dark-contrast-rgb: 0,0,0; 132 | --ion-color-dark-shade: #d7d8da; 133 | --ion-color-dark-tint: #f5f6f9; 134 | 135 | --ion-color-medium: #989aa2; 136 | --ion-color-medium-rgb: 152,154,162; 137 | --ion-color-medium-contrast: #000000; 138 | --ion-color-medium-contrast-rgb: 0,0,0; 139 | --ion-color-medium-shade: #86888f; 140 | --ion-color-medium-tint: #a2a4ab; 141 | 142 | --ion-color-light: #222428; 143 | --ion-color-light-rgb: 34,36,40; 144 | --ion-color-light-contrast: #ffffff; 145 | --ion-color-light-contrast-rgb: 255,255,255; 146 | --ion-color-light-shade: #1e2023; 147 | --ion-color-light-tint: #383a3e; 148 | } 149 | 150 | /* 151 | * iOS Dark Theme 152 | * ------------------------------------------- 153 | */ 154 | 155 | .ios body { 156 | --ion-background-color: #000000; 157 | --ion-background-color-rgb: 0,0,0; 158 | 159 | --ion-text-color: #ffffff; 160 | --ion-text-color-rgb: 255,255,255; 161 | 162 | --ion-color-step-50: #0d0d0d; 163 | --ion-color-step-100: #1a1a1a; 164 | --ion-color-step-150: #262626; 165 | --ion-color-step-200: #333333; 166 | --ion-color-step-250: #404040; 167 | --ion-color-step-300: #4d4d4d; 168 | --ion-color-step-350: #595959; 169 | --ion-color-step-400: #666666; 170 | --ion-color-step-450: #737373; 171 | --ion-color-step-500: #808080; 172 | --ion-color-step-550: #8c8c8c; 173 | --ion-color-step-600: #999999; 174 | --ion-color-step-650: #a6a6a6; 175 | --ion-color-step-700: #b3b3b3; 176 | --ion-color-step-750: #bfbfbf; 177 | --ion-color-step-800: #cccccc; 178 | --ion-color-step-850: #d9d9d9; 179 | --ion-color-step-900: #e6e6e6; 180 | --ion-color-step-950: #f2f2f2; 181 | 182 | --ion-item-background: #000000; 183 | 184 | --ion-card-background: #1c1c1d; 185 | } 186 | 187 | .ios ion-modal { 188 | --ion-background-color: var(--ion-color-step-100); 189 | --ion-toolbar-background: var(--ion-color-step-150); 190 | --ion-toolbar-border-color: var(--ion-color-step-250); 191 | } 192 | 193 | 194 | /* 195 | * Material Design Dark Theme 196 | * ------------------------------------------- 197 | */ 198 | 199 | .md body { 200 | --ion-background-color: #121212; 201 | --ion-background-color-rgb: 18,18,18; 202 | 203 | --ion-text-color: #ffffff; 204 | --ion-text-color-rgb: 255,255,255; 205 | 206 | --ion-border-color: #222222; 207 | 208 | --ion-color-step-50: #1e1e1e; 209 | --ion-color-step-100: #2a2a2a; 210 | --ion-color-step-150: #363636; 211 | --ion-color-step-200: #414141; 212 | --ion-color-step-250: #4d4d4d; 213 | --ion-color-step-300: #595959; 214 | --ion-color-step-350: #656565; 215 | --ion-color-step-400: #717171; 216 | --ion-color-step-450: #7d7d7d; 217 | --ion-color-step-500: #898989; 218 | --ion-color-step-550: #949494; 219 | --ion-color-step-600: #a0a0a0; 220 | --ion-color-step-650: #acacac; 221 | --ion-color-step-700: #b8b8b8; 222 | --ion-color-step-750: #c4c4c4; 223 | --ion-color-step-800: #d0d0d0; 224 | --ion-color-step-850: #dbdbdb; 225 | --ion-color-step-900: #e7e7e7; 226 | --ion-color-step-950: #f3f3f3; 227 | 228 | --ion-item-background: #1e1e1e; 229 | 230 | --ion-toolbar-background: #1f1f1f; 231 | 232 | --ion-tab-bar-background: #1f1f1f; 233 | 234 | --ion-card-background: #1e1e1e; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /demo/src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | // eslint-disable-next-line no-underscore-dangle 6 | (window as any).__Zone_disable_customElements = true; 7 | -------------------------------------------------------------------------------- /demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": ["es2018", "dom"] 16 | }, 17 | "angularCompilerOptions": { 18 | "enableI18nLegacyMessageIdFormat": false, 19 | "strictInjectionParameters": true, 20 | "strictInputAccessModifiers": true, 21 | "strictTemplates": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spoonconsulting/cordova-plugin-background-upload", 3 | "version": "4.1.2", 4 | "description": "Cordova plugin for uploading file in the background", 5 | "cordova": { 6 | "id": "@spoonconsulting/cordova-plugin-background-upload", 7 | "platforms": [ 8 | "android", 9 | "ios" 10 | ] 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/spoonconsulting/cordova-plugin-background-upload" 15 | }, 16 | "keywords": [ 17 | "cordova", 18 | "background", 19 | "file", 20 | "upload", 21 | "workmanager", 22 | "ecosystem:cordova", 23 | "cordova-android", 24 | "cordova-ios" 25 | ], 26 | "scripts": { 27 | "lint": "npx standard tests/tests.js && npx standard www/FileTransferManager.js", 28 | "cordova-paramedic": "cordova-paramedic", 29 | "test:android": "cordova-paramedic --platform android@8.0 --plugin . --cleanUpAfterRun", 30 | "test:ios": "cordova-paramedic --platform ios --plugin . --cleanUpAfterRun" 31 | }, 32 | "dependencies": {}, 33 | "author": "Mevin Dhunnooa & Spoon Consulting Ltd", 34 | "contributors": [ 35 | { 36 | "name": "Luc Boissaye", 37 | "email": "luc@boissaye.fr", 38 | "url": "https://github.com/ombr", 39 | "hireable": true 40 | }, 41 | { 42 | "name": "Lucas Malandrino", 43 | "url": "https://github.com/icanwalkonwater", 44 | "hireable": true 45 | }, 46 | { 47 | "name": "Azhar Beebeejaun", 48 | "email": "azhar.beebeejaun@spoonconsulting.com", 49 | "url": "https://github.com/azharbeebeejaun", 50 | "hireable": true 51 | }, 52 | { 53 | "name": "Dinitri Ragoo ", 54 | "email": "dimitriragoo@gmail.com", 55 | "url": "https://github.com/dinitri-ragoo", 56 | "hireable": true 57 | }, 58 | { 59 | "name": "Nick ANDRIANAHARIMALALA", 60 | "email": "nick.andrianaharimalala@spoonconsulting.com", 61 | "url": "https://github.com/sc-nick", 62 | "hireable": true 63 | }, 64 | { 65 | "name": "Zafir Sk Heerah", 66 | "email": "zafir.heerah@spoonconsulting.com", 67 | "url": "https://github.com/zafirskthelifehacker", 68 | "hireable": true 69 | } 70 | ], 71 | "license": "Apache-2.0", 72 | "engines": { 73 | "node": ">=16.17.1", 74 | "npm": ">=6.x", 75 | "cordova": ">=8.0.0" 76 | }, 77 | "standard": { 78 | "env": [ 79 | "mocha", 80 | "commonjs", 81 | "jasmine" 82 | ] 83 | }, 84 | "devDependencies": { 85 | "cordova-paramedic": "git+https://github.com/apache/cordova-paramedic.git", 86 | "standard": "^14.0.2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cordova Background Upload Plugin 4 | Background Upload plugin for Cordova 5 | ISC 6 | cordova,background,file,download,workmanager 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | -------------------------------------------------------------------------------- /src/android/AckDatabase.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.room.Database; 6 | import androidx.room.Room; 7 | import androidx.room.RoomDatabase; 8 | import androidx.room.TypeConverters; 9 | import androidx.work.Data; 10 | 11 | @Database(entities = {UploadEvent.class}, version = 5) 12 | @TypeConverters(value = {Data.class}) 13 | public abstract class AckDatabase extends RoomDatabase { 14 | private static AckDatabase instance; 15 | 16 | public static AckDatabase getInstance(final Context context) { 17 | if (instance == null) { 18 | instance = Room 19 | .databaseBuilder(context, AckDatabase.class, "cordova-plugin-background-upload.db") 20 | .fallbackToDestructiveMigration() 21 | .build(); 22 | } 23 | return instance; 24 | } 25 | 26 | public static void closeInstance() { 27 | // not called for now 28 | instance.close(); 29 | instance = null; 30 | } 31 | 32 | public abstract UploadEventDao uploadEventDao(); 33 | } 34 | -------------------------------------------------------------------------------- /src/android/ProgressRequestBody.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | import okhttp3.MediaType; 10 | import okhttp3.RequestBody; 11 | import okio.BufferedSink; 12 | 13 | public class ProgressRequestBody extends RequestBody { 14 | 15 | @FunctionalInterface 16 | public interface ProgressListener { 17 | void onProgress(long bytesWritten, long totalBytes); 18 | } 19 | 20 | private final MediaType mediaType; 21 | private final long contentLength; 22 | private final InputStream stream; 23 | private final ProgressListener listener; 24 | 25 | private long bytesWritten = 0; 26 | private long lastProgressTimestamp = 0; 27 | 28 | public ProgressRequestBody(final MediaType mediaType, long contentLength, final InputStream stream, final ProgressListener listener) { 29 | this.mediaType = mediaType; 30 | this.contentLength = contentLength; 31 | this.stream = stream; 32 | this.listener = listener; 33 | } 34 | 35 | @Nullable 36 | @Override 37 | public MediaType contentType() { 38 | return mediaType; 39 | } 40 | 41 | @Override 42 | public long contentLength() { 43 | return contentLength; 44 | } 45 | 46 | @Override 47 | public void writeTo(@NonNull BufferedSink bufferedSink) throws IOException { 48 | byte[] buffer = new byte[8192]; 49 | int read; 50 | while ((read = this.stream.read(buffer)) != -1) { 51 | bufferedSink.write(buffer, 0, read); 52 | 53 | // Trigger listener 54 | bytesWritten += read; 55 | 56 | // Event throttling 57 | long now = System.currentTimeMillis() / 1000; 58 | if (now - lastProgressTimestamp >= 1) { 59 | lastProgressTimestamp = now; 60 | listener.onProgress(bytesWritten, contentLength); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/android/UploadEvent.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.room.ColumnInfo; 5 | import androidx.room.Entity; 6 | import androidx.room.PrimaryKey; 7 | import androidx.work.Data; 8 | 9 | @Entity(tableName = "upload_event") 10 | public class UploadEvent { 11 | @PrimaryKey 12 | @NonNull 13 | private String id; 14 | 15 | @ColumnInfo(name = "output_data") 16 | @NonNull 17 | private Data outputData; 18 | 19 | public UploadEvent(@NonNull final String id, @NonNull final Data outputData) { 20 | this.id = id; 21 | this.outputData = outputData; 22 | } 23 | 24 | @NonNull 25 | public String getId() { 26 | return id; 27 | } 28 | 29 | @NonNull 30 | public Data getOutputData() { 31 | return outputData; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/android/UploadEventDao.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Delete; 5 | import androidx.room.Insert; 6 | import androidx.room.OnConflictStrategy; 7 | import androidx.room.Query; 8 | 9 | import java.util.List; 10 | 11 | @Dao 12 | public interface UploadEventDao { 13 | @Query("SELECT * FROM upload_event") 14 | List getAll(); 15 | 16 | @Query("SELECT * FROM upload_event WHERE id = :id") 17 | UploadEvent getById(final String id); 18 | 19 | default boolean exists(final String id) { 20 | return getById(id) != null; 21 | } 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | void insert(final UploadEvent ack); 25 | 26 | @Delete 27 | void delete(final UploadEvent ack); 28 | 29 | default void delete(final String id) { 30 | UploadEvent ack = getById(id); 31 | if (ack != null) { 32 | delete(ack); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/android/UploadForegroundNotification.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import android.app.Notification; 4 | import android.app.PendingIntent; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.graphics.Color; 8 | import android.os.Build; 9 | import android.util.Log; 10 | 11 | import androidx.annotation.IntegerRes; 12 | import androidx.core.app.NotificationCompat; 13 | import androidx.work.ForegroundInfo; 14 | import androidx.work.WorkInfo; 15 | import androidx.work.WorkManager; 16 | 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Random; 22 | import java.util.UUID; 23 | import java.util.concurrent.ExecutionException; 24 | import java.util.concurrent.atomic.AtomicLong; 25 | 26 | public class UploadForegroundNotification { 27 | private static final Map collectiveProgress = Collections.synchronizedMap(new HashMap<>()); 28 | private static final AtomicLong lastNotificationUpdateMs = new AtomicLong(0); 29 | private static ForegroundInfo cachedInfo; 30 | 31 | private static final int notificationId = new Random().nextInt(); 32 | public static String notificationTitle = "Default title"; 33 | 34 | @IntegerRes 35 | public static int notificationIconRes = 0; 36 | public static String notificationIntentActivity; 37 | 38 | public static void configure(final String title, @IntegerRes final int icon, final String intentActivity) { 39 | notificationTitle = title; 40 | notificationIconRes = icon; 41 | notificationIntentActivity = intentActivity; 42 | } 43 | 44 | public static void progress(final UUID uuid, final float progress) { 45 | collectiveProgress.put(uuid, progress); 46 | } 47 | 48 | public static void done(final UUID uuid) { 49 | collectiveProgress.remove(uuid); 50 | } 51 | 52 | static ForegroundInfo getForegroundInfo(final Context context) { 53 | final long now = System.currentTimeMillis(); 54 | // Set to now to ensure other worker will be throttled 55 | final long lastUpdate = lastNotificationUpdateMs.getAndSet(now); 56 | 57 | // Throttle, 200ms delay 58 | if (cachedInfo != null && now - lastUpdate <= UploadTask.DELAY_BETWEEN_NOTIFICATION_UPDATE_MS) { 59 | // Revert value 60 | lastNotificationUpdateMs.set(lastUpdate); 61 | return cachedInfo; 62 | } 63 | 64 | List workInfo; 65 | try { 66 | workInfo = WorkManager.getInstance(context) 67 | .getWorkInfosByTag(FileTransferBackground.getCurrentTag(context)) 68 | .get(); 69 | } catch (ExecutionException | InterruptedException e) { 70 | Log.w(UploadTask.TAG, "getForegroundInfo: Problem while retrieving task list:", e); 71 | workInfo = Collections.emptyList(); 72 | } 73 | 74 | float uploadingProgress = 0f; 75 | int uploadDone = 0; 76 | int uploadCount = 0; 77 | for (WorkInfo info : workInfo) { 78 | if (!info.getState().isFinished()) { 79 | final Float progress = collectiveProgress.get(info.getId()); 80 | if (progress != null) { 81 | uploadingProgress += progress; 82 | } 83 | } else { 84 | uploadDone++; 85 | } 86 | uploadCount++; 87 | } 88 | 89 | float totalProgressStore = ((float) uploadDone) / uploadCount; 90 | 91 | Log.d(UploadTask.TAG, "eventLabel='getForegroundInfo: general (" + uploadingProgress + ") all (" + collectiveProgress + ")'"); 92 | 93 | Class mainActivityClass = null; 94 | try { 95 | mainActivityClass = Class.forName(notificationIntentActivity); 96 | } catch (ClassNotFoundException e) { 97 | e.printStackTrace(); 98 | } 99 | Intent notificationIntent = new Intent(context, mainActivityClass); 100 | int pendingIntentFlag; 101 | if (Build.VERSION.SDK_INT >= 23) { 102 | pendingIntentFlag = PendingIntent.FLAG_IMMUTABLE; 103 | } else { 104 | pendingIntentFlag = 0; 105 | } 106 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, pendingIntentFlag); 107 | 108 | // TODO: click intent open app 109 | Notification notification = new NotificationCompat.Builder(context, UploadTask.NOTIFICATION_CHANNEL_ID) 110 | .setContentTitle(notificationTitle) 111 | .setTicker(notificationTitle) 112 | .setSmallIcon(notificationIconRes) 113 | .setColor(Color.rgb(57, 100, 150)) 114 | .setOngoing(true) 115 | .setProgress(100, (int) (totalProgressStore * 100f), false) 116 | .setContentIntent(pendingIntent) 117 | .addAction(notificationIconRes, "Open", pendingIntent) 118 | .build(); 119 | 120 | notification.flags |= Notification.FLAG_NO_CLEAR; 121 | notification.flags |= Notification.FLAG_ONGOING_EVENT; 122 | notification.flags |= Notification.FLAG_FOREGROUND_SERVICE; 123 | 124 | 125 | cachedInfo = new ForegroundInfo(notificationId, notification); 126 | return cachedInfo; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/android/UploadNotification.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.graphics.Color; 11 | import android.os.Build; 12 | import android.util.Log; 13 | 14 | import androidx.annotation.IntegerRes; 15 | import androidx.annotation.RequiresApi; 16 | import androidx.core.app.NotificationCompat; 17 | import androidx.work.WorkInfo; 18 | import androidx.work.WorkManager; 19 | 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.Random; 23 | import java.util.concurrent.ExecutionException; 24 | 25 | public class UploadNotification { 26 | private Context context; 27 | 28 | private static final int notificationId = new Random().nextInt(); 29 | public static String notificationTitle = "Default title"; 30 | public static String notificationRetryText = "Please check your internet connection"; 31 | @IntegerRes 32 | public static int notificationIconRes = 0; 33 | public static String notificationIntentActivity; 34 | 35 | public static NotificationManager notificationManager = null; 36 | public static NotificationCompat.Builder notificationBuilder = null; 37 | 38 | @RequiresApi(api = Build.VERSION_CODES.O) 39 | UploadNotification(Context context) { 40 | this.context = context; 41 | notificationBuilder = getUploadNotification(context); 42 | notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 43 | notificationManager.createNotificationChannel(new NotificationChannel( 44 | UploadTask.NOTIFICATION_CHANNEL_ID, 45 | UploadTask.NOTIFICATION_CHANNEL_NAME, 46 | NotificationManager.IMPORTANCE_LOW 47 | )); 48 | } 49 | 50 | public void updateProgress() { 51 | List workInfo; 52 | try { 53 | workInfo = WorkManager.getInstance(context) 54 | .getWorkInfosByTag(FileTransferBackground.getCurrentTag(context)) 55 | .get(); 56 | } catch (ExecutionException | InterruptedException e) { 57 | Log.w(UploadTask.TAG, "getForegroundInfo: Problem while retrieving task list:", e); 58 | workInfo = Collections.emptyList(); 59 | } 60 | 61 | int uploadDone = 0; 62 | int uploadCount = 0; 63 | for (WorkInfo info : workInfo) { 64 | if (info.getState().isFinished()) { 65 | uploadDone++; 66 | } 67 | uploadCount++; 68 | } 69 | 70 | float totalProgressStore = ((float) uploadDone) / uploadCount; 71 | notificationBuilder.setProgress(100, (int) (totalProgressStore * 100f), false); 72 | notificationManager.notify(UploadNotification.notificationId, notificationBuilder.build()); 73 | } 74 | 75 | public static void configure(final String title, @IntegerRes final int icon, final String intentActivity) { 76 | notificationTitle = title; 77 | notificationIconRes = icon; 78 | notificationIntentActivity = intentActivity; 79 | } 80 | 81 | public static Notification createNotification(NotificationCompat.Builder notificationBuilder) { 82 | Notification notification = notificationBuilder.build(); 83 | notification.flags |= Notification.FLAG_NO_CLEAR; 84 | notification.flags |= Notification.FLAG_ONGOING_EVENT; 85 | return notification; 86 | } 87 | 88 | @RequiresApi(api = Build.VERSION_CODES.O) 89 | private static NotificationCompat.Builder getUploadNotification(final Context context) { 90 | Class mainActivityClass = null; 91 | try { 92 | mainActivityClass = Class.forName(notificationIntentActivity); 93 | } catch (ClassNotFoundException e) { 94 | e.printStackTrace(); 95 | } 96 | Intent notificationIntent = new Intent(context, mainActivityClass); 97 | int pendingIntentFlag; 98 | if (Build.VERSION.SDK_INT >= 23) { 99 | pendingIntentFlag = PendingIntent.FLAG_IMMUTABLE; 100 | } else { 101 | pendingIntentFlag = 0; 102 | } 103 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, pendingIntentFlag); 104 | 105 | // TODO: click intent open app 106 | @SuppressLint("ResourceType") NotificationCompat.Builder uploadNotificationBuilder = new NotificationCompat.Builder(context, UploadTask.NOTIFICATION_CHANNEL_ID) 107 | .setContentTitle(notificationTitle) 108 | .setTicker(notificationTitle) 109 | .setSmallIcon(notificationIconRes) 110 | .setColor(Color.rgb(57, 100, 150)) 111 | .setContentIntent(pendingIntent) 112 | .setOngoing(true) 113 | .setPriority(NotificationCompat.PRIORITY_LOW) 114 | .setProgress(100, 0, false) 115 | .setChannelId(UploadTask.NOTIFICATION_CHANNEL_ID) 116 | .addAction(notificationIconRes, "Open", pendingIntent); 117 | 118 | return uploadNotificationBuilder; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/android/UploadTask.java: -------------------------------------------------------------------------------- 1 | package com.spoon.backgroundfileupload; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.os.Build; 6 | import android.util.Log; 7 | import android.webkit.MimeTypeMap; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.work.Data; 11 | import androidx.work.Worker; 12 | import androidx.work.WorkerParameters; 13 | 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.FileNotFoundException; 17 | import java.io.FileOutputStream; 18 | import java.io.IOException; 19 | import java.net.ProtocolException; 20 | import java.net.SocketException; 21 | import java.net.SocketTimeoutException; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Objects; 24 | import java.util.concurrent.Semaphore; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import javax.net.ssl.SSLException; 28 | 29 | import okhttp3.Call; 30 | import okhttp3.HttpUrl; 31 | import okhttp3.MediaType; 32 | import okhttp3.MultipartBody; 33 | import okhttp3.OkHttpClient; 34 | import okhttp3.Request; 35 | import okhttp3.Response; 36 | 37 | public final class UploadTask extends Worker { 38 | 39 | private static final boolean DEBUG_SKIP_UPLOAD = false; 40 | public static final long DELAY_BETWEEN_NOTIFICATION_UPDATE_MS = 200; 41 | 42 | public static final String TAG = "CordovaBackgroundUpload"; 43 | 44 | public static final String NOTIFICATION_CHANNEL_ID = "com.spoon.backgroundfileupload.channel"; 45 | public static final String NOTIFICATION_CHANNEL_NAME = "upload channel"; 46 | 47 | public static final int MAX_TRIES = 10; 48 | 49 | // Key stuff 50 | // 51 | 52 | // Keys used in the input data 53 | public static final String KEY_INPUT_ID = "input_id"; 54 | public static final String KEY_INPUT_URL = "input_url"; 55 | public static final String KEY_INPUT_FILEPATH = "input_filepath"; 56 | public static final String KEY_INPUT_FILE_KEY = "input_file_key"; 57 | public static final String KEY_INPUT_HTTP_METHOD = "input_http_method"; 58 | public static final String KEY_INPUT_HEADERS_COUNT = "input_headers_count"; 59 | public static final String KEY_INPUT_HEADERS_NAMES = "input_headers_names"; 60 | public static final String KEY_INPUT_HEADER_VALUE_PREFIX = "input_header_"; 61 | public static final String KEY_INPUT_PARAMETERS_COUNT = "input_parameters_count"; 62 | public static final String KEY_INPUT_PARAMETERS_NAMES = "input_parameters_names"; 63 | public static final String KEY_INPUT_PARAMETER_VALUE_PREFIX = "input_parameter_"; 64 | public static final String KEY_INPUT_NOTIFICATION_TITLE = "input_notification_title"; 65 | public static final String KEY_INPUT_NOTIFICATION_ICON = "input_notification_icon"; 66 | // Input keys but used for configuring the OkHttp instance 67 | public static final String KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS = "input_config_concurrent_downloads"; 68 | public static final String KEY_INPUT_CONFIG_INTENT_ACTIVITY = "input_config_intent_activity"; 69 | 70 | 71 | // Keys used for the progress data 72 | public static final String KEY_PROGRESS_ID = "progress_id"; 73 | public static final String KEY_PROGRESS_PERCENT = "progress_percent"; 74 | 75 | // Keys used for the result 76 | public static final String KEY_OUTPUT_ID = "output_id"; 77 | public static final String KEY_OUTPUT_IS_ERROR = "output_is_error"; 78 | public static final String KEY_OUTPUT_RESPONSE_FILE = "output_response"; 79 | public static final String KEY_OUTPUT_STATUS_CODE = "output_status_code"; 80 | public static final String KEY_OUTPUT_FAILURE_REASON = "output_failure_reason"; 81 | public static final String KEY_OUTPUT_FAILURE_CANCELED = "output_failure_canceled"; 82 | public static final String KEY_OUTPUT_UPLOAD_START_TIME = "output_upload_start_time"; 83 | public static final String KEY_OUTPUT_UPLOAD_FINISH_TIME = "output_upload_finish_time"; 84 | // 85 | 86 | private static UploadNotification uploadNotification = null; 87 | private static UploadForegroundNotification uploadForegroundNotification = null; 88 | 89 | public static class Mutex { 90 | public void acquire() throws InterruptedException { } 91 | public void release() { } 92 | } 93 | 94 | private static OkHttpClient httpClient; 95 | 96 | private Call currentCall; 97 | 98 | private static int concurrency = 1; 99 | private static Semaphore concurrentUploads = new Semaphore(concurrency, true); 100 | private static Mutex concurrencyLock = new Mutex(); 101 | long startTime = 0; 102 | long endTime = 0; 103 | 104 | public UploadTask(@NonNull Context context, @NonNull WorkerParameters workerParams) { 105 | 106 | super(context, workerParams); 107 | 108 | int concurrencyConfig = workerParams.getInputData().getInt(KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS, 1); 109 | 110 | try { 111 | concurrencyLock.acquire(); 112 | try { 113 | if (concurrency != concurrencyConfig) { 114 | concurrency = concurrencyConfig; 115 | concurrentUploads = new Semaphore(concurrencyConfig, true); 116 | } 117 | } finally { 118 | concurrencyLock.release(); 119 | } 120 | } catch (InterruptedException e) { 121 | e.printStackTrace(); 122 | } 123 | 124 | if (httpClient == null) { 125 | httpClient = new OkHttpClient.Builder() 126 | .followRedirects(true) 127 | .followSslRedirects(true) 128 | .retryOnConnectionFailure(true) 129 | .connectTimeout(15, TimeUnit.SECONDS) 130 | .writeTimeout(30, TimeUnit.SECONDS) 131 | .readTimeout(30, TimeUnit.SECONDS) 132 | .cache(null) 133 | .build(); 134 | } 135 | 136 | httpClient.dispatcher().setMaxRequests(workerParams.getInputData().getInt(KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS, 2)); 137 | 138 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 139 | UploadForegroundNotification.configure( 140 | workerParams.getInputData().getString(UploadTask.KEY_INPUT_NOTIFICATION_TITLE), 141 | getApplicationContext().getResources().getIdentifier(workerParams.getInputData().getString(KEY_INPUT_NOTIFICATION_ICON), null, null), 142 | workerParams.getInputData().getString(UploadTask.KEY_INPUT_CONFIG_INTENT_ACTIVITY) 143 | ); 144 | uploadForegroundNotification = new UploadForegroundNotification(); 145 | } else { 146 | UploadNotification.configure( 147 | workerParams.getInputData().getString(UploadTask.KEY_INPUT_NOTIFICATION_TITLE), 148 | getApplicationContext().getResources().getIdentifier(workerParams.getInputData().getString(KEY_INPUT_NOTIFICATION_ICON), null, null), 149 | workerParams.getInputData().getString(UploadTask.KEY_INPUT_CONFIG_INTENT_ACTIVITY) 150 | ); 151 | uploadNotification = new UploadNotification(getApplicationContext()); 152 | } 153 | } 154 | 155 | @NonNull 156 | @Override 157 | public Result doWork() { 158 | if(!hasNetworkConnection()) { 159 | return Result.retry(); 160 | } 161 | 162 | final String id = getInputData().getString(KEY_INPUT_ID); 163 | 164 | if (id == null) { 165 | Log.e(TAG, "doWork: ID is invalid !"); 166 | return Result.failure(); 167 | } 168 | 169 | // Check retry count 170 | if (getRunAttemptCount() > MAX_TRIES) { 171 | return Result.success(new Data.Builder() 172 | .putString(KEY_OUTPUT_ID, id) 173 | .putBoolean(KEY_OUTPUT_IS_ERROR, true) 174 | .putString(KEY_OUTPUT_FAILURE_REASON, "Too many retries") 175 | .putBoolean(KEY_OUTPUT_FAILURE_CANCELED, false) 176 | .build() 177 | ); 178 | } 179 | 180 | Request request = null; 181 | try { 182 | request = createRequest(); 183 | } catch (FileNotFoundException e) { 184 | Log.e(TAG, "doWork: File not found !", e); 185 | return Result.success(new Data.Builder() 186 | .putString(KEY_OUTPUT_ID, id) 187 | .putBoolean(KEY_OUTPUT_IS_ERROR, true) 188 | .putString(KEY_OUTPUT_FAILURE_REASON, "File not found !") 189 | .putBoolean(KEY_OUTPUT_FAILURE_CANCELED, false) 190 | .build() 191 | ); 192 | } catch (NullPointerException e) { 193 | return Result.retry(); 194 | } 195 | 196 | startTime = System.currentTimeMillis(); 197 | // Register me 198 | uploadForegroundNotification.progress(getId(), 0f); 199 | handleNotification(); 200 | 201 | // Start call 202 | currentCall = httpClient.newCall(request); 203 | 204 | // Block until call is finished (or cancelled) 205 | Response response = null; 206 | try { 207 | if (!DEBUG_SKIP_UPLOAD) { 208 | try { 209 | try { 210 | concurrentUploads.acquire(); 211 | try { 212 | response = currentCall.execute(); 213 | } catch (SocketTimeoutException e) { 214 | return Result.retry(); 215 | } finally { 216 | concurrentUploads.release(); 217 | } 218 | } catch (InterruptedException e) { 219 | return Result.retry(); 220 | } 221 | } catch (SocketException | ProtocolException | SSLException e) { 222 | currentCall.cancel(); 223 | return Result.retry(); 224 | } 225 | } else { 226 | for (int i = 0; i < 10; i++) { 227 | handleProgress(i * 100, 1000); 228 | // Can be interrupted 229 | Thread.sleep(200); 230 | if (isStopped()) { 231 | throw new InterruptedException("Stopped"); 232 | } 233 | } 234 | } 235 | } catch (IOException | InterruptedException e) { 236 | // If it was user cancelled its ok 237 | // See #handleProgress for cancel code 238 | if (isStopped()) { 239 | final Data data = new Data.Builder() 240 | .putString(KEY_OUTPUT_ID, id) 241 | .putBoolean(KEY_OUTPUT_IS_ERROR, true) 242 | .putString(KEY_OUTPUT_FAILURE_REASON, "User cancelled") 243 | .putBoolean(KEY_OUTPUT_FAILURE_CANCELED, true) 244 | .build(); 245 | AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(id, data)); 246 | return Result.success(data); 247 | } else { 248 | // But if it was not it must be a connectivity problem or 249 | // something similar so we retry later 250 | Log.e(TAG, "doWork: Call failed, retrying later", e); 251 | return Result.retry(); 252 | } 253 | } finally { 254 | endTime = System.currentTimeMillis(); 255 | // Always remove ourselves from the notification 256 | uploadForegroundNotification.done(getId()); 257 | } 258 | 259 | // Start building the output data 260 | final Data.Builder outputData = new Data.Builder() 261 | .putString(KEY_OUTPUT_ID, id) 262 | .putBoolean(KEY_OUTPUT_IS_ERROR, false) 263 | .putInt(KEY_OUTPUT_STATUS_CODE, (!DEBUG_SKIP_UPLOAD) ? response.code() : 200) 264 | .putLong(KEY_OUTPUT_UPLOAD_START_TIME, startTime) 265 | .putLong(KEY_OUTPUT_UPLOAD_FINISH_TIME, endTime); 266 | 267 | // Try read the response body, if any 268 | try { 269 | final String res; 270 | if (!DEBUG_SKIP_UPLOAD) { 271 | res = response.body() != null ? response.body().string() : ""; 272 | } else { 273 | res = "heyo"; 274 | } 275 | final String filename = "upload-response-" + getId() + ".cached-response"; 276 | 277 | try (FileOutputStream fos = getApplicationContext().openFileOutput(filename, Context.MODE_PRIVATE)) { 278 | fos.write(res.getBytes(StandardCharsets.UTF_8)); 279 | } 280 | 281 | outputData.putString(KEY_OUTPUT_RESPONSE_FILE, filename); 282 | 283 | } catch (IOException e) { 284 | // Should never happen, but if it does it has something to do with reading the response 285 | Log.e(TAG, "doWork: Error while reading the response body", e); 286 | 287 | // But recover and replace the body with something else 288 | outputData.putString(KEY_OUTPUT_RESPONSE_FILE, null); 289 | } 290 | 291 | final Data data = outputData.build(); 292 | AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(id, data)); 293 | return Result.success(data); 294 | } 295 | 296 | /** 297 | * Called internally by the custom request body provider each time 8kio are written. 298 | */ 299 | private void handleProgress(long bytesWritten, long totalBytes) { 300 | // The cancel mechanism is best-effort and wont actually halt work, we need to 301 | // take care of it ourselves. 302 | if (isStopped()) { 303 | currentCall.cancel(); 304 | return; 305 | } 306 | 307 | float percent = (float) bytesWritten / (float) totalBytes; 308 | UploadForegroundNotification.progress(getId(), percent); 309 | 310 | Log.i(TAG, "handleProgress: " + getId() + " Progress: " + (int) (percent * 100f)); 311 | 312 | final Data data = new Data.Builder() 313 | .putString(KEY_PROGRESS_ID, getInputData().getString(KEY_INPUT_ID)) 314 | .putInt(KEY_PROGRESS_PERCENT, (int) (percent * 100f)) 315 | .build(); 316 | Log.d(TAG, "handleProgress: Progress data: " + data); 317 | setProgressAsync(data); 318 | handleNotification(); 319 | } 320 | 321 | /** 322 | * Create the OkHttp request that will be used, already filled with input data. 323 | * 324 | * @return A ready to use OkHttp request 325 | * @throws FileNotFoundException If the file to upload can't be found 326 | */ 327 | @NonNull 328 | private Request createRequest() throws FileNotFoundException { 329 | final String filepath = getInputData().getString(KEY_INPUT_FILEPATH); 330 | assert filepath != null; 331 | final String fileKey = getInputData().getString(KEY_INPUT_FILE_KEY); 332 | assert fileKey != null; 333 | 334 | // Build URL 335 | HttpUrl url = Objects.requireNonNull(HttpUrl.parse(getInputData().getString(KEY_INPUT_URL))).newBuilder().build(); 336 | 337 | // Build file reader 338 | String extension = MimeTypeMap.getFileExtensionFromUrl(filepath); 339 | MediaType mediaType; 340 | if (extension.equals("json") || extension.equals("db")) { 341 | // Does not support devices less than Android 10 (Stop Execution) 342 | // https://stackoverflow.com/questions/44667125/getmimetypefromextension-returns-null-when-i-pass-json-as-extension 343 | mediaType = MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + "; charset=utf-8"); 344 | } else { 345 | mediaType = MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)); 346 | } 347 | File file = new File(filepath); 348 | ProgressRequestBody fileRequestBody = new ProgressRequestBody(mediaType, file.length(), new FileInputStream(file), this::handleProgress); 349 | 350 | // Build body 351 | final MultipartBody.Builder bodyBuilder = new MultipartBody.Builder(); 352 | 353 | // With the parameters 354 | final int parametersCount = getInputData().getInt(KEY_INPUT_PARAMETERS_COUNT, 0); 355 | if (parametersCount > 0) { 356 | final String[] parameterNames = getInputData().getStringArray(KEY_INPUT_PARAMETERS_NAMES); 357 | assert parameterNames != null; 358 | 359 | for (int i = 0; i < parametersCount; i++) { 360 | final String key = parameterNames[i]; 361 | final Object value = getInputData().getKeyValueMap().get(KEY_INPUT_PARAMETER_VALUE_PREFIX + i); 362 | 363 | bodyBuilder.addFormDataPart(key, value.toString()); 364 | } 365 | } 366 | 367 | bodyBuilder.addFormDataPart(fileKey, filepath, fileRequestBody); 368 | bodyBuilder.setType(MultipartBody.FORM); 369 | 370 | // Start build request 371 | String method = getInputData().getString(KEY_INPUT_HTTP_METHOD); 372 | if (method == null) { 373 | method = "POST"; 374 | } 375 | Request.Builder requestBuilder = new Request.Builder() 376 | .url(url) 377 | .method(method.toUpperCase(), bodyBuilder.build()); 378 | 379 | // Write headers 380 | final int headersCount = getInputData().getInt(KEY_INPUT_HEADERS_COUNT, 0); 381 | final String[] headerNames = getInputData().getStringArray(KEY_INPUT_HEADERS_NAMES); 382 | assert headerNames != null; 383 | for (int i = 0; i < headersCount; i++) { 384 | final String key = headerNames[i]; 385 | final Object value = getInputData().getKeyValueMap().get(KEY_INPUT_HEADER_VALUE_PREFIX + i); 386 | 387 | requestBuilder.addHeader(key, value.toString()); 388 | } 389 | 390 | // Ok 391 | return requestBuilder.build(); 392 | } 393 | 394 | private void handleNotification() { 395 | Log.d(TAG, "Upload Notification"); 396 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 397 | setForegroundAsync(uploadForegroundNotification.getForegroundInfo(getApplicationContext())); 398 | } else { 399 | uploadNotification.updateProgress(); 400 | } 401 | Log.d(TAG, "Upload Notification Exit"); 402 | } 403 | 404 | private synchronized boolean hasNetworkConnection() { 405 | ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); 406 | if((connectivityManager == null) || (connectivityManager.getActiveNetworkInfo() == null) || (connectivityManager.getActiveNetworkInfo().isConnectedOrConnecting() == false)) { 407 | Log.d(TAG, "No internet connection"); 408 | return false; 409 | } 410 | return true; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/android/config.gradle: -------------------------------------------------------------------------------- 1 | def minSdkVersion = 21 2 | if(cordovaConfig.MIN_SDK_VERSION < minSdkVersion){ 3 | ext.cdvMinSdkVersion = minSdkVersion 4 | } 5 | 6 | dependencies { 7 | annotationProcessor 'androidx.room:room-compiler:2.5.0' 8 | } 9 | -------------------------------------------------------------------------------- /src/android/res/ic_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/src/android/res/ic_upload.png -------------------------------------------------------------------------------- /src/ios/FileTransferBackground.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "FileUploader.h" 4 | 5 | @interface FileTransferBackground : CDVPlugin 6 | -(void)startUpload:(CDVInvokedUrlCommand*)command; 7 | -(void)removeUpload:(CDVInvokedUrlCommand*)command; 8 | -(void)initManager:(CDVInvokedUrlCommand*)command; 9 | -(void)acknowledgeEvent:(CDVInvokedUrlCommand*)command; 10 | -(void)destroy:(CDVInvokedUrlCommand*)command; 11 | @end 12 | -------------------------------------------------------------------------------- /src/ios/FileTransferBackground.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "FileTransferBackground.h" 3 | #import 4 | #import "FileUploader.h" 5 | 6 | @interface FileTransferBackground() 7 | @property (nonatomic, strong) CDVInvokedUrlCommand* pluginCommand; 8 | @end 9 | @implementation FileTransferBackground 10 | -(void)initManager:(CDVInvokedUrlCommand*)command{ 11 | [self runBlockInBackgroundWithTryCatch:^{ 12 | self.pluginCommand = command; 13 | if (command.arguments.count > 0){ 14 | NSDictionary* config = command.arguments[0]; 15 | FileUploader.parallelUploadsLimit = ((NSNumber*)config[@"parallelUploadsLimit"]).integerValue; 16 | } 17 | 18 | [FileUploader sharedInstance].delegate = self; 19 | //mark all old uploads as failed to be retried 20 | for (NSString* uploadId in [self getV1Uploads]){ 21 | [self sendCallback:@{ 22 | @"state" : @"FAILED", 23 | @"id" : uploadId, 24 | @"error": @"upload failed", 25 | @"errorCode" : @500 26 | }]; 27 | } 28 | 29 | for (UploadEvent* event in [UploadEvent allEvents]){ 30 | [self uploadManagerDidReceiveCallback: [event dataRepresentation]]; 31 | } 32 | } forCommand:command]; 33 | } 34 | 35 | -(void)startUpload:(CDVInvokedUrlCommand*)command{ 36 | [self runBlockInBackgroundWithTryCatch:^{ 37 | NSDictionary* payload = command.arguments[0]; 38 | __weak FileTransferBackground *weakSelf = self; 39 | [[FileUploader sharedInstance] addUpload:payload 40 | completionHandler:^(NSError* error) { 41 | if (error){ 42 | [weakSelf sendCallback:@{ 43 | @"error" : error.localizedDescription, 44 | @"id" : payload[@"id"], 45 | @"errorCode" : @(error.code) 46 | }]; 47 | } 48 | }]; 49 | } forCommand:command]; 50 | } 51 | 52 | -(void)removeUpload:(CDVInvokedUrlCommand*)command{ 53 | [self runBlockInBackgroundWithTryCatch:^{ 54 | [[FileUploader sharedInstance] removeUpload:command.arguments[0]]; 55 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 56 | [pluginResult setKeepCallback:@YES]; 57 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 58 | } forCommand:command]; 59 | } 60 | 61 | -(void)uploadManagerDidReceiveCallback:(NSDictionary*)info{ 62 | [self sendCallback:info]; 63 | } 64 | 65 | -(void)sendCallback:(NSDictionary*)data{ 66 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data]; 67 | [pluginResult setKeepCallback:@YES]; 68 | [self.commandDelegate sendPluginResult:pluginResult callbackId:self.pluginCommand.callbackId]; 69 | } 70 | 71 | -(void)runBlockInBackgroundWithTryCatch:(void (^)(void))block forCommand:(CDVInvokedUrlCommand*)command{ 72 | [self.commandDelegate runInBackground:^{ 73 | @try { 74 | block(); 75 | } @catch (NSException *exception) { 76 | [self sendErrorCallback:command forException:exception]; 77 | } 78 | }]; 79 | } 80 | 81 | -(void)acknowledgeEvent:(CDVInvokedUrlCommand*)command{ 82 | [self runBlockInBackgroundWithTryCatch:^{ 83 | [[FileUploader sharedInstance] acknowledgeEventReceived:command.arguments[0]]; 84 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 85 | [pluginResult setKeepCallback:@YES]; 86 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 87 | } forCommand:command]; 88 | } 89 | 90 | -(void)destroy:(CDVInvokedUrlCommand*)command{ 91 | self.pluginCommand = nil; 92 | } 93 | 94 | -(NSArray*)getV1Uploads{ 95 | //returns uploads made by older version of the plugin 96 | NSMutableArray* oldUploadIds = [[NSMutableArray alloc] init]; 97 | NSURL* cachess = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL]; 98 | NSURL* workDirectoryURL = [cachess URLByAppendingPathComponent:@"FileUploadManager"]; 99 | NSArray* directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:workDirectoryURL 100 | includingPropertiesForKeys:@[NSURLIsDirectoryKey] 101 | options:NSDirectoryEnumerationSkipsSubdirectoryDescendants 102 | error:nil]; 103 | for (NSURL * itemURL in directoryContents) { 104 | NSString* directoryName = [itemURL lastPathComponent]; 105 | if ([directoryName hasPrefix:@"Upload-"]) { 106 | NSString* name = [[directoryName componentsSeparatedByString:@"*"] firstObject]; 107 | NSString* uploadId = [name stringByReplacingOccurrencesOfString:@"Upload-" withString:@""]; 108 | [oldUploadIds addObject:uploadId]; 109 | } 110 | } 111 | //remove the old uploads directory 112 | [[NSFileManager defaultManager] removeItemAtURL:workDirectoryURL error:nil]; 113 | return oldUploadIds; 114 | } 115 | 116 | -(void)sendErrorCallback:(CDVInvokedUrlCommand*)command forException:(NSException*)exception{ 117 | NSString* message = [NSString stringWithFormat:@"(%@) - %@", exception.name, exception.reason]; 118 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus: CDVCommandStatus_ERROR messageAsString:message] callbackId:command.callbackId]; 119 | } 120 | @end 121 | -------------------------------------------------------------------------------- /src/ios/FileUploader.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "UploadEvent.h" 3 | #import 4 | #import 5 | NS_ASSUME_NONNULL_BEGIN 6 | @protocol FileUploaderDelegate 7 | @optional 8 | -(void)uploadManagerDidReceiveCallback:(NSDictionary*)info; 9 | @end 10 | 11 | @interface FileUploader : NSObject 12 | @property (nonatomic, strong) id delegate; 13 | +(instancetype)sharedInstance; 14 | -(void)addUpload:(NSDictionary *)payload completionHandler:(void (^)(NSError* error))handler; 15 | -(void)removeUpload:(NSString*)uploadId; 16 | -(void)acknowledgeEventReceived:(NSString*)eventId; 17 | @property (class, nonatomic, assign) NSInteger parallelUploadsLimit; 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /src/ios/FileUploader.m: -------------------------------------------------------------------------------- 1 | #import "FileUploader.h" 2 | @interface FileUploader() 3 | @property (nonatomic, strong) NSMutableDictionary *uploadStartTimes; 4 | @property (nonatomic, strong) NSMutableDictionary *responsesData; 5 | @property (nonatomic, strong) AFURLSessionManager *manager; 6 | @end 7 | 8 | @implementation FileUploader 9 | static NSInteger _parallelUploadsLimit = 1; 10 | static FileUploader *singletonObject = nil; 11 | static NSString * kUploadUUIDStrPropertyKey = @"com.spoonconsulting.plugin-background-upload.UUID"; 12 | +(instancetype)sharedInstance{ 13 | if (!singletonObject) 14 | singletonObject = [[FileUploader alloc] init]; 15 | return singletonObject; 16 | } 17 | 18 | -(id)init{ 19 | self = [super init]; 20 | if (self == nil) 21 | return nil; 22 | [UploadEvent setupStorage]; 23 | self.responsesData = [[NSMutableDictionary alloc] init]; 24 | self.uploadStartTimes = [[NSMutableDictionary alloc] init]; 25 | NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSBundle mainBundle] bundleIdentifier]]; 26 | configuration.HTTPMaximumConnectionsPerHost = FileUploader.parallelUploadsLimit; 27 | configuration.sessionSendsLaunchEvents = NO; 28 | self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; 29 | __weak FileUploader *weakSelf = self; 30 | [self.manager setTaskDidCompleteBlock:^(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSError * _Nullable error) { 31 | NSString* uploadId = [NSURLProtocol propertyForKey:kUploadUUIDStrPropertyKey inRequest:task.originalRequest]; 32 | NSDate *startTime = weakSelf.uploadStartTimes[uploadId]; 33 | NSDate *endUploadTime = [NSDate date]; 34 | NSTimeInterval timeInterval = [endUploadTime timeIntervalSince1970]; 35 | long long endUploadTimeInMS = (long long)(timeInterval * 1000); 36 | NSTimeInterval duration = [endUploadTime timeIntervalSinceDate:startTime]; 37 | NSLog(@"[BackgroundUpload] Task %@ completed with error %@", uploadId, error); 38 | if (!error){ 39 | NSData* serverData = weakSelf.responsesData[@(task.taskIdentifier)]; 40 | NSString* serverResponse = serverData ? [[NSString alloc] initWithData:serverData encoding:NSUTF8StringEncoding] : @""; 41 | [weakSelf.responsesData removeObjectForKey:@(task.taskIdentifier)]; 42 | NSMutableDictionary *event = [@{ 43 | @"id" : uploadId, 44 | @"state" : @"UPLOADED", 45 | @"statusCode" : @(((NSHTTPURLResponse *)task.response).statusCode), 46 | @"serverResponse" : serverResponse 47 | } mutableCopy]; 48 | 49 | if (!isnan(duration)) { 50 | event[@"uploadDuration"] = @(duration * 1000); 51 | event[@"finishUploadTime"] = @(endUploadTimeInMS); 52 | } 53 | 54 | [weakSelf saveAndSendEvent:event]; 55 | } else { 56 | [weakSelf.responsesData removeObjectForKey:@(task.taskIdentifier)]; 57 | [weakSelf saveAndSendEvent:@{ 58 | @"id" : uploadId, 59 | @"state" : @"FAILED", 60 | @"error" : error.localizedDescription, 61 | @"errorCode" : @(error.code), 62 | }]; 63 | } 64 | }]; 65 | 66 | [self.manager setDataTaskDidReceiveDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDataTask * _Nonnull dataTask, NSData * _Nonnull data) { 67 | NSMutableData *responseData = weakSelf.responsesData[@(dataTask.taskIdentifier)]; 68 | if (!responseData) { 69 | weakSelf.responsesData[@(dataTask.taskIdentifier)] = [NSMutableData dataWithData:data]; 70 | } else { 71 | [responseData appendData:data]; 72 | } 73 | }]; 74 | return self; 75 | } 76 | 77 | -(void)saveAndSendEvent:(NSDictionary*)data{ 78 | UploadEvent* event = [UploadEvent create:data]; 79 | [self sendEvent:[event dataRepresentation]]; 80 | } 81 | 82 | -(void)sendEvent:(NSDictionary*)info{ 83 | [self.delegate uploadManagerDidReceiveCallback:info]; 84 | } 85 | 86 | +(NSInteger)parallelUploadsLimit { 87 | return _parallelUploadsLimit; 88 | } 89 | 90 | +(void)setParallelUploadsLimit:(NSInteger)value { 91 | _parallelUploadsLimit = value; 92 | } 93 | 94 | -(void)addUpload:(NSDictionary *)payload completionHandler:(void (^)(NSError* error))handler{ 95 | __weak FileUploader *weakSelf = self; 96 | NSURL *tempFilePath = [self tempFilePathForUpload:payload[@"id"]]; 97 | [self writeMultipartDataToTempFile:tempFilePath 98 | url:[NSURL URLWithString:payload[@"serverUrl"]] 99 | uploadId:payload[@"id"] 100 | fileURL:[NSURL fileURLWithPath:payload[@"filePath"]] 101 | headers: payload[@"headers"] 102 | parameters:payload[@"parameters"] 103 | fileKey:payload[@"fileKey"] 104 | completionHandler:^(NSError *error, NSMutableURLRequest *request) { 105 | if (error) 106 | return handler(error); 107 | 108 | weakSelf.uploadStartTimes[payload[@"id"]] = [NSDate date]; 109 | 110 | __block double lastProgressTimeStamp = 0; 111 | 112 | [[weakSelf.manager uploadTaskWithRequest:request 113 | fromFile:tempFilePath 114 | progress:^(NSProgress * _Nonnull uploadProgress) 115 | { 116 | float roundedProgress = roundf(10 * (uploadProgress.fractionCompleted*100)) / 10.0; 117 | NSLog(@"[BackgroundUpload] Task %@ progression %f", [NSURLProtocol propertyForKey:kUploadUUIDStrPropertyKey inRequest:request], roundedProgress); 118 | NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; 119 | if (currentTimestamp - lastProgressTimeStamp >= 1){ 120 | lastProgressTimeStamp = currentTimestamp; 121 | [weakSelf sendEvent:@{ 122 | @"progress" : @(roundedProgress), 123 | @"id" : [NSURLProtocol propertyForKey:kUploadUUIDStrPropertyKey inRequest:request], 124 | @"state": @"UPLOADING" 125 | }]; 126 | } 127 | } 128 | completionHandler:nil] resume]; 129 | [[NSFileManager defaultManager] removeItemAtURL:[weakSelf tempFilePathForUpload:payload[@"id"]] error:nil]; 130 | }]; 131 | } 132 | 133 | -(NSURL*)tempFilePathForUpload:(NSString*)uploadId{ 134 | NSString* path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0]; 135 | return [NSURL fileURLWithPath:[path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.request",uploadId]]]; 136 | } 137 | 138 | -(void)writeMultipartDataToTempFile: (NSURL*)tempFilePath 139 | url:(NSURL *)url 140 | uploadId:(NSString*)uploadId 141 | fileURL:(NSURL *)fileURL 142 | headers:(NSDictionary*)headers 143 | parameters:(NSDictionary*)parameters 144 | fileKey:(NSString*)fileKey 145 | completionHandler:(void (^)(NSError* error, NSMutableURLRequest* request))handler{ 146 | AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer]; 147 | NSError *error; 148 | NSMutableURLRequest *request = 149 | [serializer multipartFormRequestWithMethod:@"POST" 150 | URLString:url.absoluteString 151 | parameters:parameters 152 | constructingBodyWithBlock:^(id formData) 153 | { 154 | NSString *filename = [fileURL.absoluteString lastPathComponent]; 155 | NSData * data = [NSData dataWithContentsOfURL:fileURL]; 156 | [formData appendPartWithFileData:data name:fileKey fileName:filename mimeType:@"application/octet-stream"]; 157 | } 158 | error:&error]; 159 | if (error) 160 | return handler(error, nil); 161 | for (NSString *key in headers) { 162 | [request setValue:[headers objectForKey:key] forHTTPHeaderField:key]; 163 | } 164 | [NSURLProtocol setProperty:uploadId forKey:kUploadUUIDStrPropertyKey inRequest:request]; 165 | [serializer requestWithMultipartFormRequest:request writingStreamContentsToFile:tempFilePath completionHandler:^(NSError *error) { 166 | if (!error && ![[NSFileManager defaultManager] fileExistsAtPath:tempFilePath.path]) 167 | error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoSuchFileError userInfo:nil]; 168 | handler(error, request); 169 | }]; 170 | } 171 | 172 | -(void)removeUpload:(NSString*)uploadId{ 173 | NSURLSessionUploadTask *correspondingTask = 174 | [[self.manager.uploadTasks filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(NSURLSessionUploadTask* task, NSDictionary *bindings) { 175 | NSString* currentId = [NSURLProtocol propertyForKey:kUploadUUIDStrPropertyKey inRequest:task.originalRequest]; 176 | return [uploadId isEqualToString:currentId]; 177 | }]] firstObject]; 178 | [correspondingTask cancel]; 179 | } 180 | 181 | -(void)acknowledgeEventReceived:(NSString*)eventId{ 182 | [[UploadEvent eventWithId:eventId] destroy]; 183 | } 184 | @end 185 | -------------------------------------------------------------------------------- /src/ios/UploadEvent.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface UploadEvent : NSManagedObject 6 | 7 | -(void)save; 8 | -(void)destroy; 9 | +(UploadEvent*)eventWithId:(NSString*)eventId; 10 | +(NSArray*)allEvents; 11 | +(void)setupStorage; 12 | +(UploadEvent*)create:(NSDictionary*)info; 13 | -(NSDictionary*)dataRepresentation; 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /src/ios/UploadEvent.m: -------------------------------------------------------------------------------- 1 | #import "UploadEvent.h" 2 | @interface UploadEvent() 3 | @property (nonatomic, strong) NSString* data; 4 | @end 5 | @implementation UploadEvent 6 | @synthesize data; 7 | static NSManagedObjectContext * managedObjectContext; 8 | static NSPersistentStoreCoordinator * persistentStoreCoordinator; 9 | -(id)init{ 10 | NSEntityDescription *entity = [NSEntityDescription entityForName:@"UploadEvent" inManagedObjectContext:managedObjectContext]; 11 | self = [super initWithEntity:entity insertIntoManagedObjectContext:managedObjectContext]; 12 | if (self == nil) 13 | return nil; 14 | return self; 15 | } 16 | 17 | -(void)save{ 18 | [managedObjectContext performBlockAndWait:^{ 19 | NSError* error; 20 | if (![managedObjectContext save:&error]) 21 | NSLog(@"error saving UploadEvent %@ : %@", self, error); 22 | }]; 23 | } 24 | 25 | -(void)destroy{ 26 | [managedObjectContext performBlock:^{ 27 | [managedObjectContext deleteObject:self]; 28 | NSError* error; 29 | if (![managedObjectContext save:&error]) 30 | NSLog(@"error deleting UploadEvent %@ : %@", self, error); 31 | }]; 32 | } 33 | 34 | -(NSDictionary*)dataRepresentation{ 35 | NSData *data = [self.data dataUsingEncoding:NSUTF8StringEncoding]; 36 | NSMutableDictionary* dictRepresentation = [[NSJSONSerialization JSONObjectWithData:data options:0 error:nil] mutableCopy]; 37 | [dictRepresentation addEntriesFromDictionary: @{ 38 | @"eventId" : self.objectID.URIRepresentation.absoluteString 39 | }]; 40 | return dictRepresentation; 41 | } 42 | 43 | +(UploadEvent*)eventWithId:(NSString*)eventId{ 44 | NSManagedObjectID* objectId = [persistentStoreCoordinator managedObjectIDForURIRepresentation: [NSURL URLWithString:eventId]]; 45 | return objectId ? [managedObjectContext objectWithID:objectId] : nil; 46 | } 47 | 48 | +(NSArray*)allEvents{ 49 | NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"UploadEvent"]; 50 | request.returnsObjectsAsFaults = NO; 51 | return [managedObjectContext executeFetchRequest:request error:NULL]; 52 | } 53 | 54 | +(UploadEvent*)create:(NSDictionary*)info{ 55 | UploadEvent* event = [[UploadEvent alloc] init]; 56 | NSData * jsonData = [NSJSONSerialization dataWithJSONObject:info options:0 error:nil]; 57 | event.data = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 58 | [event save]; 59 | return event; 60 | } 61 | 62 | +(void)setupStorage{ 63 | NSString* path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0]; 64 | NSURL *storeURL = [NSURL fileURLWithPath:[path stringByAppendingPathComponent:@"Background-upload-plugin.db"]]; 65 | NSError *error = nil; 66 | persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self tableRepresentation]]; 67 | if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) 68 | NSLog(@"error setting up core data: %@", error); 69 | managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 70 | managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; 71 | [managedObjectContext setPersistentStoreCoordinator:persistentStoreCoordinator]; 72 | } 73 | 74 | +(NSManagedObjectModel*)tableRepresentation{ 75 | NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init]; 76 | NSEntityDescription *entity = [[NSEntityDescription alloc] init]; 77 | [entity setName:@"UploadEvent"]; 78 | [entity setManagedObjectClassName:@"UploadEvent"]; 79 | NSAttributeDescription *fileDataAttribute = [[NSAttributeDescription alloc] init]; 80 | [fileDataAttribute setName:@"data"]; 81 | [fileDataAttribute setAttributeType:NSStringAttributeType]; 82 | [fileDataAttribute setOptional:NO]; 83 | [entity setProperties:@[fileDataAttribute]]; 84 | [model setEntities:@[entity]]; 85 | return model; 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "version": "0.0.1", 4 | "description": "tests for cordova-plugin-background-upload", 5 | "cordova": { 6 | "id": "cordova-plugin-background-upload-tests", 7 | "platforms": [] 8 | }, 9 | "keywords": [ 10 | "ecosystem:cordova" 11 | ], 12 | "author": "Spoon Consulting Ltd", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /tests/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | cordova-plugin-background-upload tests 9 | Cordova plugin for background upload 10 | Apache 2.0 11 | https://github.com/spoonconsulting/cordova-plugin-background-upload.git 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/test-server/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | .vscode 6 | uploads -------------------------------------------------------------------------------- /tests/test-server/app.js: -------------------------------------------------------------------------------- 1 | const PORT = process.env.PORT || 3000 2 | const express = require('express') 3 | const Busboy = require('busboy') 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const handleUpload = (req, res, next) => { 8 | const busboy = new Busboy({ headers: req.headers }) 9 | let response = { 10 | originalFilename: null, 11 | accessMode: 'public', 12 | height: 4048, 13 | grayscale: false, 14 | width: 3036, 15 | headers: req.headers, 16 | parameters: {} 17 | } 18 | 19 | busboy.on('file', function(fieldName, file, fileName) { 20 | response.originalFilename = fileName 21 | file.pipe(fs.createWriteStream(path.join('./uploads', fileName))); 22 | }); 23 | 24 | busboy.on('field', function(fieldName, value) { 25 | response.parameters[fieldName] = value; 26 | }); 27 | 28 | busboy.on('finish', function() { 29 | res.status(req.method == 'POST' ? 201 : 200).send(JSON.stringify({ receivedInfo: response })) 30 | }); 31 | 32 | return req.pipe(busboy); 33 | } 34 | 35 | const app = express() 36 | 37 | app.post('/upload', handleUpload) 38 | app.put('/upload', handleUpload) 39 | 40 | app.listen(PORT, () => console.log(`Listening on ${PORT}`)) 41 | -------------------------------------------------------------------------------- /tests/test-server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "busboy": { 22 | "version": "0.3.1", 23 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", 24 | "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", 25 | "requires": { 26 | "dicer": "0.3.0" 27 | } 28 | }, 29 | "bytes": { 30 | "version": "3.0.0", 31 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 32 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 33 | }, 34 | "content-disposition": { 35 | "version": "0.5.2", 36 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 37 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 38 | }, 39 | "content-type": { 40 | "version": "1.0.4", 41 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 42 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 43 | }, 44 | "cookie": { 45 | "version": "0.3.1", 46 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 47 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 48 | }, 49 | "cookie-signature": { 50 | "version": "1.0.6", 51 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 52 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 53 | }, 54 | "debug": { 55 | "version": "2.6.9", 56 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 57 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 58 | "requires": { 59 | "ms": "2.0.0" 60 | } 61 | }, 62 | "depd": { 63 | "version": "1.1.2", 64 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 65 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 66 | }, 67 | "destroy": { 68 | "version": "1.0.4", 69 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 70 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 71 | }, 72 | "dicer": { 73 | "version": "0.3.0", 74 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", 75 | "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", 76 | "requires": { 77 | "streamsearch": "0.1.2" 78 | } 79 | }, 80 | "ee-first": { 81 | "version": "1.1.1", 82 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 83 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 84 | }, 85 | "encodeurl": { 86 | "version": "1.0.2", 87 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 88 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 89 | }, 90 | "escape-html": { 91 | "version": "1.0.3", 92 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 93 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 94 | }, 95 | "etag": { 96 | "version": "1.8.1", 97 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 98 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 99 | }, 100 | "express": { 101 | "version": "4.16.3", 102 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 103 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 104 | "requires": { 105 | "accepts": "~1.3.5", 106 | "array-flatten": "1.1.1", 107 | "body-parser": "1.18.2", 108 | "content-disposition": "0.5.2", 109 | "content-type": "~1.0.4", 110 | "cookie": "0.3.1", 111 | "cookie-signature": "1.0.6", 112 | "debug": "2.6.9", 113 | "depd": "~1.1.2", 114 | "encodeurl": "~1.0.2", 115 | "escape-html": "~1.0.3", 116 | "etag": "~1.8.1", 117 | "finalhandler": "1.1.1", 118 | "fresh": "0.5.2", 119 | "merge-descriptors": "1.0.1", 120 | "methods": "~1.1.2", 121 | "on-finished": "~2.3.0", 122 | "parseurl": "~1.3.2", 123 | "path-to-regexp": "0.1.7", 124 | "proxy-addr": "~2.0.3", 125 | "qs": "6.5.1", 126 | "range-parser": "~1.2.0", 127 | "safe-buffer": "5.1.1", 128 | "send": "0.16.2", 129 | "serve-static": "1.13.2", 130 | "setprototypeof": "1.1.0", 131 | "statuses": "~1.4.0", 132 | "type-is": "~1.6.16", 133 | "utils-merge": "1.0.1", 134 | "vary": "~1.1.2" 135 | }, 136 | "dependencies": { 137 | "body-parser": { 138 | "version": "1.18.2", 139 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 140 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 141 | "requires": { 142 | "bytes": "3.0.0", 143 | "content-type": "~1.0.4", 144 | "debug": "2.6.9", 145 | "depd": "~1.1.1", 146 | "http-errors": "~1.6.2", 147 | "iconv-lite": "0.4.19", 148 | "on-finished": "~2.3.0", 149 | "qs": "6.5.1", 150 | "raw-body": "2.3.2", 151 | "type-is": "~1.6.15" 152 | } 153 | }, 154 | "iconv-lite": { 155 | "version": "0.4.19", 156 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 157 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 158 | }, 159 | "raw-body": { 160 | "version": "2.3.2", 161 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 162 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 163 | "requires": { 164 | "bytes": "3.0.0", 165 | "http-errors": "1.6.2", 166 | "iconv-lite": "0.4.19", 167 | "unpipe": "1.0.0" 168 | }, 169 | "dependencies": { 170 | "depd": { 171 | "version": "1.1.1", 172 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 173 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 174 | }, 175 | "http-errors": { 176 | "version": "1.6.2", 177 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 178 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 179 | "requires": { 180 | "depd": "1.1.1", 181 | "inherits": "2.0.3", 182 | "setprototypeof": "1.0.3", 183 | "statuses": ">= 1.3.1 < 2" 184 | } 185 | }, 186 | "setprototypeof": { 187 | "version": "1.0.3", 188 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 189 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 190 | } 191 | } 192 | } 193 | } 194 | }, 195 | "finalhandler": { 196 | "version": "1.1.1", 197 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 198 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 199 | "requires": { 200 | "debug": "2.6.9", 201 | "encodeurl": "~1.0.2", 202 | "escape-html": "~1.0.3", 203 | "on-finished": "~2.3.0", 204 | "parseurl": "~1.3.2", 205 | "statuses": "~1.4.0", 206 | "unpipe": "~1.0.0" 207 | } 208 | }, 209 | "forwarded": { 210 | "version": "0.1.2", 211 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 212 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 213 | }, 214 | "fresh": { 215 | "version": "0.5.2", 216 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 217 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 218 | }, 219 | "http-errors": { 220 | "version": "1.6.3", 221 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 222 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 223 | "requires": { 224 | "depd": "~1.1.2", 225 | "inherits": "2.0.3", 226 | "setprototypeof": "1.1.0", 227 | "statuses": ">= 1.4.0 < 2" 228 | } 229 | }, 230 | "inherits": { 231 | "version": "2.0.3", 232 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 233 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 234 | }, 235 | "ipaddr.js": { 236 | "version": "1.8.0", 237 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 238 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 239 | }, 240 | "media-typer": { 241 | "version": "0.3.0", 242 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 243 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 244 | }, 245 | "merge-descriptors": { 246 | "version": "1.0.1", 247 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 248 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 249 | }, 250 | "methods": { 251 | "version": "1.1.2", 252 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 253 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 254 | }, 255 | "mime": { 256 | "version": "1.4.1", 257 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 258 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 259 | }, 260 | "mime-db": { 261 | "version": "1.35.0", 262 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 263 | "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" 264 | }, 265 | "mime-types": { 266 | "version": "2.1.19", 267 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 268 | "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", 269 | "requires": { 270 | "mime-db": "~1.35.0" 271 | } 272 | }, 273 | "ms": { 274 | "version": "2.0.0", 275 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 276 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 277 | }, 278 | "negotiator": { 279 | "version": "0.6.1", 280 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 281 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 282 | }, 283 | "on-finished": { 284 | "version": "2.3.0", 285 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 286 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 287 | "requires": { 288 | "ee-first": "1.1.1" 289 | } 290 | }, 291 | "parseurl": { 292 | "version": "1.3.2", 293 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 294 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 295 | }, 296 | "path-to-regexp": { 297 | "version": "0.1.7", 298 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 299 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 300 | }, 301 | "proxy-addr": { 302 | "version": "2.0.4", 303 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 304 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 305 | "requires": { 306 | "forwarded": "~0.1.2", 307 | "ipaddr.js": "1.8.0" 308 | } 309 | }, 310 | "qs": { 311 | "version": "6.5.1", 312 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 313 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 314 | }, 315 | "range-parser": { 316 | "version": "1.2.0", 317 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 318 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 319 | }, 320 | "safe-buffer": { 321 | "version": "5.1.1", 322 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 323 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 324 | }, 325 | "send": { 326 | "version": "0.16.2", 327 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 328 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 329 | "requires": { 330 | "debug": "2.6.9", 331 | "depd": "~1.1.2", 332 | "destroy": "~1.0.4", 333 | "encodeurl": "~1.0.2", 334 | "escape-html": "~1.0.3", 335 | "etag": "~1.8.1", 336 | "fresh": "0.5.2", 337 | "http-errors": "~1.6.2", 338 | "mime": "1.4.1", 339 | "ms": "2.0.0", 340 | "on-finished": "~2.3.0", 341 | "range-parser": "~1.2.0", 342 | "statuses": "~1.4.0" 343 | } 344 | }, 345 | "serve-static": { 346 | "version": "1.13.2", 347 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 348 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 349 | "requires": { 350 | "encodeurl": "~1.0.2", 351 | "escape-html": "~1.0.3", 352 | "parseurl": "~1.3.2", 353 | "send": "0.16.2" 354 | } 355 | }, 356 | "setprototypeof": { 357 | "version": "1.1.0", 358 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 359 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 360 | }, 361 | "statuses": { 362 | "version": "1.4.0", 363 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 364 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 365 | }, 366 | "streamsearch": { 367 | "version": "0.1.2", 368 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 369 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 370 | }, 371 | "type-is": { 372 | "version": "1.6.16", 373 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 374 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 375 | "requires": { 376 | "media-typer": "0.3.0", 377 | "mime-types": "~2.1.18" 378 | } 379 | }, 380 | "unpipe": { 381 | "version": "1.0.0", 382 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 383 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 384 | }, 385 | "utils-merge": { 386 | "version": "1.0.1", 387 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 388 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 389 | }, 390 | "vary": { 391 | "version": "1.1.2", 392 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 393 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /tests/test-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-server", 3 | "version": "1.0.0", 4 | "description": "test server for cordova-plugin-background-upload", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "forever start app.js" 9 | }, 10 | "author": "Spoon Consulting Ltd", 11 | "license": "CC-BY-NC-SA-3.0", 12 | "dependencies": { 13 | "busboy": "^0.3.1", 14 | "express": "^4.14.0" 15 | }, 16 | "engines": { 17 | "node": "10.9.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/test-utils.js: -------------------------------------------------------------------------------- 1 | /* global cordova */ 2 | var TestUtils = (function () { 3 | function copyFileToDataDirectory (fileName) { 4 | return new Promise(function (resolve, reject) { 5 | window.resolveLocalFileSystemURL(cordova.file.applicationDirectory + fileName, function (fileEntry) { 6 | window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (directory) { 7 | fileEntry.copyTo(directory, fileName, function () { 8 | resolve((cordova.file.dataDirectory + fileName)) 9 | }, 10 | function (err) { 11 | console.log(err) 12 | //file already exist 13 | resolve((cordova.file.dataDirectory + fileName)) 14 | }) 15 | }, reject) 16 | }, reject) 17 | }) 18 | } 19 | 20 | function deleteFile (fileName) { 21 | return new Promise(function (resolve, reject) { 22 | window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (dir) { 23 | dir.getFile(fileName, { 24 | create: false 25 | }, function (fileEntry) { 26 | fileEntry.remove(resolve, reject, reject) 27 | }) 28 | }, reject) 29 | }) 30 | } 31 | 32 | return { 33 | copyFileToDataDirectory: copyFileToDataDirectory, 34 | deleteFile: deleteFile 35 | } 36 | })() 37 | module.exports = TestUtils 38 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | /* global FileTransferManager, TestUtils */ 2 | 3 | exports.defineAutoTests = function () { 4 | describe('Uploader', function () { 5 | var originalTimeout 6 | var sampleFile = 'tree.jpg' 7 | var path = '' 8 | var serverHost = window.cordova.platformId === 'android' ? '10.0.2.2' : 'localhost' 9 | var serverUrl = 'http://' + serverHost + ':3000/upload' 10 | var nativeUploader 11 | 12 | beforeEach(function (done) { 13 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL 14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 15 | TestUtils.copyFileToDataDirectory(sampleFile).then(function (newPath) { 16 | path = newPath 17 | done() 18 | }) 19 | }) 20 | 21 | afterEach(function (done) { 22 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout 23 | if (nativeUploader) { 24 | nativeUploader.destroy() 25 | } 26 | TestUtils.deleteFile(sampleFile).then(done) 27 | }) 28 | 29 | describe('Add upload', function () { 30 | it('exposes FileTransferManager globally', function () { 31 | expect(FileTransferManager).toBeDefined() 32 | }) 33 | 34 | it('should have init function', function () { 35 | expect(FileTransferManager.init).toBeDefined() 36 | }) 37 | 38 | it('should have startUpload function', function (done) { 39 | nativeUploader = FileTransferManager.init({}, function () { 40 | expect(nativeUploader.startUpload).toBeDefined() 41 | done() 42 | }) 43 | }) 44 | 45 | it('returns an error if no argument is given', function (done) { 46 | nativeUploader = FileTransferManager.init({}, function () { 47 | nativeUploader.startUpload(null, null, function (result) { 48 | expect(result.error).toBe('Upload Settings object is missing or has invalid arguments') 49 | done() 50 | }) 51 | }) 52 | }) 53 | 54 | it('returns an error if upload id is missing', function (done) { 55 | nativeUploader = FileTransferManager.init({}, function () { 56 | nativeUploader.startUpload({ }, null, function (result) { 57 | expect(result.error).toBe('Upload ID is required') 58 | done() 59 | }) 60 | }) 61 | }) 62 | 63 | it('returns an error if serverUrl is missing', function (done) { 64 | nativeUploader = FileTransferManager.init({}, function () { 65 | nativeUploader.startUpload({ id: 'test_id', filePath: path }, null, function (result) { 66 | expect(result.id).toBe('test_id') 67 | expect(result.error).toBe('Server URL is required') 68 | done() 69 | }) 70 | }) 71 | }) 72 | 73 | it('returns an error if serverUrl is invalid', function (done) { 74 | nativeUploader = FileTransferManager.init({}, function () { 75 | nativeUploader.startUpload({ id: '123_456', serverUrl: ' ' }, null, function (result) { 76 | expect(result).toBeDefined() 77 | expect(result.id).toBe('123_456') 78 | expect(result.error).toBe('Invalid server URL') 79 | done() 80 | }) 81 | }) 82 | }) 83 | 84 | it('returns an error if filePath is missing', function (done) { 85 | nativeUploader = FileTransferManager.init({}, function () { 86 | nativeUploader.startUpload({ id: 'some_id', serverUrl: serverUrl }, null, function (result) { 87 | expect(result.id).toBe('some_id') 88 | expect(result.error).toBe('filePath is required') 89 | done() 90 | }) 91 | }) 92 | }) 93 | 94 | it('sends upload progress events', function (done) { 95 | nativeUploader = FileTransferManager.init({}, function (upload) { 96 | if (upload.state === 'INITIALIZED') { 97 | nativeUploader.startUpload({ id: 'a_file_id', serverUrl: serverUrl, filePath: path }) 98 | } else if (upload.state === 'UPLOADED') { 99 | nativeUploader.acknowledgeEvent(upload.eventId, done) 100 | } else if (upload.state === 'UPLOADING') { 101 | expect(upload.id).toBe('a_file_id') 102 | expect(typeof upload.progress).toBe('number') 103 | expect(upload.eventId).toBeUndefined() 104 | expect(upload.error).toBeUndefined() 105 | } 106 | }) 107 | }) 108 | 109 | it('sends success callback when upload is completed', function (done) { 110 | nativeUploader = FileTransferManager.init({}, function (upload) { 111 | if (upload.state === 'INITIALIZED') { 112 | nativeUploader.startUpload({ id: 'abc', serverUrl: serverUrl, filePath: path }) 113 | } else if (upload.state === 'UPLOADED') { 114 | expect(upload.id).toBe('abc') 115 | expect(upload.eventId).toBeDefined() 116 | expect(upload.error).toBeUndefined() 117 | expect(upload.statusCode).toBe(201) 118 | var response = JSON.parse(upload.serverResponse) 119 | delete response.receivedInfo.headers 120 | expect(response.receivedInfo).toEqual({ 121 | originalFilename: sampleFile, 122 | accessMode: 'public', 123 | height: 4048, 124 | grayscale: false, 125 | width: 3036, 126 | parameters: {} 127 | }) 128 | nativeUploader.acknowledgeEvent(upload.eventId, done) 129 | } 130 | }) 131 | }) 132 | 133 | it('upload success with put method', function (done) { 134 | nativeUploader = FileTransferManager.init({}, function (upload) { 135 | if (upload.state === 'INITIALIZED') { 136 | nativeUploader.startUpload({ id: 'file_id', serverUrl: serverUrl, filePath: path, requestMethod: 'PUT' }) 137 | } else if (upload.state === 'UPLOADED') { 138 | expect(upload.id).toBe('file_id') 139 | expect(upload.statusCode).toBe(200) 140 | expect(upload.eventId).toBeDefined() 141 | expect(upload.error).toBeUndefined() 142 | var response = JSON.parse(upload.serverResponse) 143 | delete response.receivedInfo.headers 144 | expect(response.receivedInfo).toEqual({ 145 | originalFilename: sampleFile, 146 | accessMode: 'public', 147 | height: 4048, 148 | grayscale: false, 149 | width: 3036, 150 | parameters: {} 151 | }) 152 | nativeUploader.acknowledgeEvent(upload.eventId, done) 153 | } 154 | }) 155 | }) 156 | 157 | it('sends headers during upload', function (done) { 158 | var headers = { signature: 'secret_hash', source: 'test' } 159 | nativeUploader = FileTransferManager.init({}, function (upload) { 160 | if (upload.state === 'INITIALIZED') { 161 | nativeUploader.startUpload({ id: 'plop', serverUrl: serverUrl, filePath: path, headers: headers }) 162 | } else if (upload.state === 'UPLOADED') { 163 | expect(upload.id).toBe('plop') 164 | var response = JSON.parse(upload.serverResponse) 165 | expect(response.receivedInfo.headers.signature).toBe('secret_hash') 166 | expect(response.receivedInfo.headers.source).toBe('test') 167 | nativeUploader.acknowledgeEvent(upload.eventId, done) 168 | } 169 | }) 170 | }) 171 | 172 | it('sends parameters during upload', function (done) { 173 | var params = { 174 | role: 'tester', 175 | type: 'authenticated' 176 | } 177 | nativeUploader = FileTransferManager.init({}, function (upload) { 178 | if (upload.state === 'INITIALIZED') { 179 | nativeUploader.startUpload({ id: 'xeon', serverUrl: serverUrl, filePath: path, parameters: params }) 180 | } else if (upload.state === 'UPLOADED') { 181 | expect(upload.id).toBe('xeon') 182 | var response = JSON.parse(upload.serverResponse) 183 | expect(response.receivedInfo.parameters).toEqual(params) 184 | nativeUploader.acknowledgeEvent(upload.eventId, done) 185 | } 186 | }) 187 | }) 188 | 189 | it('sends a FAILED event if upload fails', function (done) { 190 | nativeUploader = FileTransferManager.init({}, function (upload) { 191 | if (upload.state === 'INITIALIZED') { 192 | nativeUploader.startUpload({ id: 'err_id', serverUrl: 'dummy_url', filePath: path }) 193 | } else if (upload.state === 'FAILED') { 194 | expect(upload.id).toBe('err_id') 195 | expect(upload.error).toBeDefined() 196 | expect(upload.errorCode).toBeDefined() 197 | nativeUploader.acknowledgeEvent(upload.eventId, done) 198 | } 199 | }) 200 | }) 201 | 202 | it('sends a FAILED callback if file does not exist', function (done) { 203 | nativeUploader = FileTransferManager.init({}, function (upload) { 204 | if (upload.state === 'INITIALIZED') { 205 | nativeUploader.startUpload({ id: 'nox', serverUrl: serverUrl, filePath: '/path/fake.jpg' }, function () {}, function (upload) { 206 | expect(upload.id).toBe('nox') 207 | expect(upload.eventId).toBeUndefined() 208 | expect(upload.error).toContain('File not found') 209 | done() 210 | }) 211 | } 212 | }) 213 | }) 214 | }) 215 | 216 | describe('Multiple Upload', function () { 217 | var sampleFile2 = 'tree2.jpg'; var sampleFile3 = 'tree3.jpg'; var path2 = ''; var path3 = '' 218 | 219 | beforeEach(function (done) { 220 | TestUtils.copyFileToDataDirectory(sampleFile2).then(function (newPath2) { 221 | path2 = newPath2 222 | TestUtils.copyFileToDataDirectory(sampleFile3).then(function (newPath3) { 223 | path3 = newPath3 224 | done() 225 | }) 226 | }) 227 | }) 228 | 229 | afterEach(function (done) { 230 | TestUtils.deleteFile(sampleFile2).then(function () { 231 | TestUtils.deleteFile(sampleFile3).then(done) 232 | }) 233 | }) 234 | 235 | it('can transfer in parallel', function (done) { 236 | var filesToUpload = ['file_1', 'file_2', 'file_3'] 237 | var uploadedFiles = [] 238 | nativeUploader = FileTransferManager.init({ parallelUploadsLimit: 3 }, function (upload) { 239 | if (upload.state === 'INITIALIZED') { 240 | nativeUploader.startUpload({ id: filesToUpload[0], serverUrl: serverUrl, filePath: path }) 241 | nativeUploader.startUpload({ id: filesToUpload[1], serverUrl: serverUrl, filePath: path2 }) 242 | nativeUploader.startUpload({ id: filesToUpload[2], serverUrl: serverUrl, filePath: path3 }) 243 | } else if (upload.state === 'UPLOADED') { 244 | expect(filesToUpload).toContain(upload.id) 245 | if (uploadedFiles.indexOf(upload.id) < 0 && filesToUpload.indexOf(upload.id) > -1) { 246 | uploadedFiles.push(upload.id) 247 | nativeUploader.acknowledgeEvent(upload.eventId, function () { 248 | if (uploadedFiles.length >= 3) done() 249 | }) 250 | } 251 | } 252 | }) 253 | }) 254 | }) 255 | 256 | describe('Remove upload', function () { 257 | it('should have removeUpload function', function (done) { 258 | nativeUploader = FileTransferManager.init({}, function () { 259 | expect(nativeUploader.removeUpload).toBeDefined() 260 | done() 261 | }) 262 | }) 263 | 264 | it('returns an error if no uploadId is given', function (done) { 265 | nativeUploader = FileTransferManager.init({}, function () { 266 | nativeUploader.removeUpload(null, null, function (result) { 267 | expect(result.error).toBe('Upload ID is required') 268 | done() 269 | }) 270 | }) 271 | }) 272 | 273 | it('returns an error if undefined uploadId is given', function (done) { 274 | nativeUploader = FileTransferManager.init({}, function (upload) { 275 | if (upload.state === 'INITIALIZED') { 276 | nativeUploader.removeUpload(undefined, null, function (result) { 277 | expect(result.error).toBe('Upload ID is required') 278 | done() 279 | }) 280 | } 281 | }) 282 | }) 283 | 284 | it('does not return error if uploadId is given', function (done) { 285 | nativeUploader = FileTransferManager.init({}, function (upload) { 286 | if (upload.state === 'INITIALIZED') { 287 | nativeUploader.removeUpload('blob', function () { 288 | expect(true).toBeTruthy() 289 | done() 290 | }, null) 291 | } 292 | }) 293 | }) 294 | 295 | it('sends a FAILED callback when upload is removed', function (done) { 296 | nativeUploader = FileTransferManager.init({}, function (upload) { 297 | if (upload.state === 'INITIALIZED') { 298 | nativeUploader.startUpload({ id: 'xyz', serverUrl: serverUrl, filePath: path }) 299 | } else if (upload.state === 'FAILED') { 300 | expect(upload.id).toBe('xyz') 301 | expect(upload.eventId).toBeDefined() 302 | expect(upload.error).toContain('cancel') 303 | expect(upload.errorCode).toBe(-999) 304 | nativeUploader.acknowledgeEvent(upload.eventId, done) 305 | } else if (upload.state === 'UPLOADING') { 306 | nativeUploader.removeUpload('xyz', null, null) 307 | } 308 | }) 309 | }) 310 | }) 311 | 312 | describe('Acknowledge event', function () { 313 | it('should have acknowledgeEvent function', function (done) { 314 | nativeUploader = FileTransferManager.init({}, function () { 315 | expect(nativeUploader.acknowledgeEvent).toBeDefined() 316 | done() 317 | }) 318 | }) 319 | 320 | it('returns an error if no eventId is given', function (done) { 321 | nativeUploader = FileTransferManager.init({}, function () { 322 | nativeUploader.acknowledgeEvent(null, null, function (result) { 323 | expect(result.error).toBe('Event ID is required') 324 | done() 325 | }) 326 | }) 327 | }) 328 | 329 | it('returns an error if undefined eventId is given', function (done) { 330 | nativeUploader = FileTransferManager.init({}, function (upload) { 331 | if (upload.state === 'INITIALIZED') { 332 | nativeUploader.acknowledgeEvent(undefined, null, function (result) { 333 | expect(result.error).toBe('Event ID is required') 334 | done() 335 | }) 336 | } 337 | }) 338 | }) 339 | 340 | it('does not return error if eventId is given', function (done) { 341 | nativeUploader = FileTransferManager.init({}, function (upload) { 342 | if (upload.state === 'INITIALIZED') { 343 | nativeUploader.acknowledgeEvent('x-coredata://123/UploadEvent/p1', function () { 344 | expect(true).toBeTruthy() 345 | done() 346 | }, null) 347 | } 348 | }) 349 | }) 350 | 351 | it('persist event id until it is acknowledged', function (done) { 352 | nativeUploader = FileTransferManager.init({}, function (upload1) { 353 | if (upload1.state === 'INITIALIZED') { 354 | nativeUploader.startUpload({ id: 'unsub', serverUrl: serverUrl, filePath: path }) 355 | } else if (upload1.state === 'UPLOADED') { 356 | nativeUploader.destroy() 357 | nativeUploader = FileTransferManager.init({}, function (upload2) { 358 | if (upload2.state !== 'INITIALIZED') { 359 | expect(upload2.id).toBe('unsub') 360 | expect(upload2.eventId).toBe(upload1.eventId) 361 | nativeUploader.acknowledgeEvent(upload2.eventId, done) 362 | } 363 | }) 364 | } 365 | }) 366 | }) 367 | }) 368 | }) 369 | } 370 | -------------------------------------------------------------------------------- /tests/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset 3 | set -o errexit 4 | 5 | npm install -g cordova@9.0.0 npx@10.2.2 forever@3.0.0 6 | npm install 7 | 8 | # lint 9 | npm run lint 10 | # start mock server 11 | cd tests/test-server && mkdir uploads && npm install && npm start 12 | cd ../.. 13 | mkdir ~/test_results 14 | # run tests appropriate for platform 15 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 16 | gem install cocoapods 17 | pod repo update 18 | npm install -g ios-sim@9.0.0 ios-deploy@1.10.0 19 | npm run test:ios 20 | fi 21 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 22 | echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a 23 | emulator -avd test -no-audio -no-window & 24 | android-wait-for-emulator 25 | npm run test:android 26 | fi -------------------------------------------------------------------------------- /tests/tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/tests/tree.jpg -------------------------------------------------------------------------------- /tests/tree2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/tests/tree2.jpg -------------------------------------------------------------------------------- /tests/tree3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonconsulting/cordova-plugin-background-upload/774e330176cc9b922c646ee06e312b94948b781a/tests/tree3.jpg -------------------------------------------------------------------------------- /www/FileTransferManager.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec') 2 | 3 | var FileTransferManager = function (options, callback) { 4 | this.options = options 5 | if (!this.options.parallelUploadsLimit) { 6 | this.options.parallelUploadsLimit = 1 7 | } 8 | 9 | if (typeof callback !== 'function') { 10 | throw new Error('event handler must be a function') 11 | } 12 | 13 | this.callback = callback 14 | exec(this.callback, null, 'FileTransferBackground', 'initManager', [this.options]) 15 | } 16 | 17 | FileTransferManager.prototype.startUpload = function (payload) { 18 | if (!payload) { 19 | return this.callback({ state: 'FAILED', error: 'Upload Settings object is missing or has invalid arguments' }) 20 | } 21 | 22 | if (!payload.id) { 23 | return this.callback({ state: 'FAILED', error: 'Upload ID is required' }) 24 | } 25 | 26 | if (!payload.serverUrl) { 27 | return this.callback({ id: payload.id, state: 'FAILED', error: 'Server URL is required' }) 28 | } 29 | 30 | if (payload.serverUrl.trim() === '') { 31 | return this.callback({ id: payload.id, state: 'FAILED', error: 'Invalid server URL' }) 32 | } 33 | 34 | if (!payload.filePath) { 35 | return this.callback({ id: payload.id, state: 'FAILED', error: 'filePath is required' }) 36 | } 37 | 38 | if (!payload.fileKey) { 39 | payload.fileKey = 'file' 40 | } 41 | 42 | payload.notificationTitle = payload.notificationTitle || 'Uploading files' 43 | 44 | if (!payload.headers) { 45 | payload.headers = {} 46 | } 47 | 48 | if (!payload.parameters) { 49 | payload.parameters = {} 50 | } 51 | 52 | var self = this 53 | window.resolveLocalFileSystemURL(payload.filePath, function (entry) { 54 | payload.filePath = new URL(entry.nativeURL).pathname.replace(/^\/local-filesystem/, '') 55 | exec(self.callback, null, 'FileTransferBackground', 'startUpload', [payload]) 56 | }, function () { 57 | self.callback({ id: payload.id, state: 'FAILED', error: 'File not found: ' + payload.filePath }) 58 | }) 59 | } 60 | 61 | FileTransferManager.prototype.removeUpload = function (id, successCb, errorCb) { 62 | if (!id) { 63 | if (errorCb) { 64 | errorCb({ error: 'Upload ID is required' }) 65 | } 66 | } else { 67 | exec(successCb, errorCb, 'FileTransferBackground', 'removeUpload', [id]) 68 | } 69 | } 70 | 71 | FileTransferManager.prototype.acknowledgeEvent = function (id, successCb, errorCb) { 72 | if (!id) { 73 | if (errorCb) { 74 | errorCb({ error: 'Event ID is required' }) 75 | } 76 | } else { 77 | exec(successCb, errorCb, 'FileTransferBackground', 'acknowledgeEvent', [id]) 78 | } 79 | } 80 | 81 | FileTransferManager.prototype.destroy = function (successCb, errorCb) { 82 | this.callback = null 83 | exec(successCb, errorCb, 'FileTransferBackground', 'destroy', []) 84 | } 85 | 86 | module.exports = { 87 | init: function (options, cb) { 88 | return new FileTransferManager(options || {}, cb) 89 | }, 90 | FileTransferManager: FileTransferManager 91 | } 92 | --------------------------------------------------------------------------------