├── .eslintrc ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── README.md ├── __mocks__ └── RNBackgroundDownloader.js ├── __tests__ └── mainTest.js ├── android ├── .classpath ├── .project ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── eko │ ├── RNBGDTaskConfig.java │ ├── RNBackgroundDownloaderModule.java │ └── RNBackgroundDownloaderPackage.java ├── babel.config.js ├── index.js ├── ios ├── RNBGDTaskConfig.h ├── RNBackgroundDownloader.h ├── RNBackgroundDownloader.m └── RNBackgroundDownloader.xcodeproj │ └── project.pbxproj ├── lib └── downloadTask.js ├── metro.config.js ├── package.json ├── react-native-background-downloader.podspec ├── react-native.config.js ├── testApp ├── .buckconfig ├── App.js ├── Style.js ├── android │ ├── .project │ ├── app │ │ ├── .project │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── testapp │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── keystores │ │ ├── BUCK │ │ └── debug.keystore.properties │ └── settings.gradle ├── app.json ├── index.js └── ios │ ├── testApp-tvOS │ └── Info.plist │ ├── testApp-tvOSTests │ └── Info.plist │ ├── testApp.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── testApp-tvOS.xcscheme │ │ └── testApp.xcscheme │ ├── testApp │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m │ └── testAppTests │ └── Info.plist └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * Eko's ESLint JSON Config file (eslint allows JavaScript-style comments in JSON config files). 3 | */ 4 | 5 | { 6 | // Enable the ESLint recommended rules as a starting point. 7 | // These are rules that report common problems, see https://eslint.org/docs/rules/ 8 | "extends": "eslint:recommended", 9 | 10 | "parser": "babel-eslint", 11 | // Specify the envs (an environment defines global variables that are predefined). 12 | // See https://eslint.org/docs/user-guide/configuring#specifying-environments 13 | "env": { 14 | // Browser global variables. 15 | "browser": false, 16 | 17 | // Node.js global variables and Node.js scoping. 18 | "node": true, 19 | 20 | // CommonJS global variables and CommonJS scoping (use this for browser-only code that uses Browserify/WebPack). 21 | "commonjs": true, 22 | 23 | // Globals common to both Node.js and Browser. 24 | "shared-node-browser": true, 25 | 26 | // enable all ECMAScript 6 features except for modules (this automatically sets the ecmaVersion parser option to 6). 27 | "es6": true, 28 | 29 | // web workers global variables. 30 | "worker": true 31 | }, 32 | 33 | // https://eslint.org/docs/rules 34 | "rules": { 35 | 36 | /////////////////////////////////////////////////////////////////////////////////// 37 | // Possible Errors https://eslint.org/docs/rules/#possible-errors 38 | /////////////////////////////////////////////////////////////////////////////////// 39 | 40 | // https://eslint.org/docs/rules/no-console 41 | "no-console": ["error", { "allow": ["error"] }], 42 | 43 | // https://eslint.org/docs/rules/valid-jsdoc 44 | // TODO - valid-jsdoc 45 | 46 | /////////////////////////////////////////////////////////////////////////////////// 47 | // Best Practices https://eslint.org/docs/rules/#best-practices 48 | /////////////////////////////////////////////////////////////////////////////////// 49 | 50 | // https://eslint.org/docs/rules/accessor-pairs 51 | "accessor-pairs": "error", 52 | 53 | // https://eslint.org/docs/rules/array-callback-return 54 | "array-callback-return": ["error", { "allowImplicit": true }], 55 | 56 | // https://eslint.org/docs/rules/block-scoped-var 57 | "block-scoped-var": "error", 58 | 59 | // https://eslint.org/docs/rules/curly 60 | "curly": ["error", "all"], 61 | 62 | // https://eslint.org/docs/rules/default-case 63 | "default-case": "error", 64 | 65 | // https://eslint.org/docs/rules/dot-location 66 | "dot-location": ["error", "property"], 67 | 68 | // https://eslint.org/docs/rules/dot-notation 69 | "dot-notation": "error", 70 | 71 | // https://eslint.org/docs/rules/eqeqeq 72 | "eqeqeq": ["error", "always"], 73 | 74 | // https://eslint.org/docs/rules/no-alert 75 | "no-alert": "error", 76 | 77 | // https://eslint.org/docs/rules/no-caller 78 | "no-caller": "error", 79 | 80 | // https://eslint.org/docs/rules/no-else-return 81 | "no-else-return": ["error", { "allowElseIf": false }], 82 | 83 | // https://eslint.org/docs/rules/no-eq-null 84 | "no-eq-null": "error", 85 | 86 | // https://eslint.org/docs/rules/no-eval 87 | "no-eval": "error", 88 | 89 | // https://eslint.org/docs/rules/no-extra-bind 90 | "no-extra-bind": "error", 91 | 92 | // https://eslint.org/docs/rules/no-fallthrough 93 | "no-fallthrough": ["error", { "commentPattern": "fall-?thr(ough|u)" }], 94 | 95 | // https://eslint.org/docs/rules/no-floating-decimal 96 | "no-floating-decimal": "error", 97 | 98 | // https://eslint.org/docs/rules/no-implicit-coercion 99 | "no-implicit-coercion": ["error", { "allow": ["!!"] }], 100 | 101 | // https://eslint.org/docs/rules/no-implicit-globals 102 | "no-implicit-globals": "error", 103 | 104 | // https://eslint.org/docs/rules/no-implied-eval 105 | "no-implied-eval": "error", 106 | 107 | // https://eslint.org/docs/rules/no-invalid-this 108 | "no-invalid-this": "error", 109 | 110 | // https://eslint.org/docs/rules/no-iterator 111 | "no-iterator": "error", 112 | 113 | // https://eslint.org/docs/rules/no-labels 114 | "no-labels": "error", 115 | 116 | // https://eslint.org/docs/rules/no-lone-blocks 117 | "no-lone-blocks": "error", 118 | 119 | // https://eslint.org/docs/rules/no-loop-func 120 | "no-loop-func": "error", 121 | 122 | // https://eslint.org/docs/rules/no-magic-numbers 123 | "no-magic-numbers": ["error", { 124 | "ignore": [ 125 | 0, 1, -1, 126 | 10, 100, 1000, 10000, 100000, 1000000, 127 | 1024, 8, 2, 128 | 24, 60 129 | ] 130 | }], 131 | 132 | // https://eslint.org/docs/rules/no-multi-spaces 133 | "no-multi-spaces": ["error", { "ignoreEOLComments": true, "exceptions": { 134 | "Property": true, 135 | "VariableDeclarator": true, 136 | "ImportDeclaration": true 137 | }}], 138 | 139 | // https://eslint.org/docs/rules/no-multi-str 140 | "no-multi-str": "error", 141 | 142 | // https://eslint.org/docs/rules/no-new 143 | "no-new": "error", 144 | 145 | // https://eslint.org/docs/rules/no-new-wrappers 146 | "no-new-wrappers": "error", 147 | 148 | // https://eslint.org/docs/rules/no-octal-escape 149 | "no-octal-escape": "error", 150 | 151 | // https://eslint.org/docs/rules/no-proto 152 | "no-proto": "error", 153 | 154 | // https://eslint.org/docs/rules/no-return-assign 155 | "no-return-assign": "error", 156 | 157 | // https://eslint.org/docs/rules/no-return-await 158 | "no-return-await": "error", 159 | 160 | // https://eslint.org/docs/rules/no-script-url 161 | "no-script-url": "error", 162 | 163 | // https://eslint.org/docs/rules/no-self-compare 164 | "no-self-compare": "error", 165 | 166 | // https://eslint.org/docs/rules/no-sequences 167 | "no-sequences": "error", 168 | 169 | // https://eslint.org/docs/rules/no-throw-literal 170 | "no-throw-literal": "error", 171 | 172 | // https://eslint.org/docs/rules/no-unmodified-loop-condition 173 | "no-unmodified-loop-condition": "error", 174 | 175 | // https://eslint.org/docs/rules/no-unused-expressions 176 | "no-unused-expressions": "error", 177 | 178 | // https://eslint.org/docs/rules/no-useless-call 179 | "no-useless-call": "error", 180 | 181 | // https://eslint.org/docs/rules/no-useless-concat 182 | "no-useless-concat": "error", 183 | 184 | // https://eslint.org/docs/rules/no-useless-return 185 | "no-useless-return": "error", 186 | 187 | // https://eslint.org/docs/rules/no-void 188 | "no-void": "error", 189 | 190 | // https://eslint.org/docs/rules/no-with 191 | "no-with": "error", 192 | 193 | // https://eslint.org/docs/rules/prefer-promise-reject-errors 194 | "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], 195 | 196 | // https://eslint.org/docs/rules/radix 197 | "radix": ["error", "always"], 198 | 199 | // https://eslint.org/docs/rules/require-await 200 | "require-await": "error", 201 | 202 | // https://eslint.org/docs/rules/wrap-iife 203 | "wrap-iife": ["error", "any"], 204 | 205 | /////////////////////////////////////////////////////////////////////////////////// 206 | // Strict Mode https://eslint.org/docs/rules/#strict-mode 207 | /////////////////////////////////////////////////////////////////////////////////// 208 | 209 | // https://eslint.org/docs/rules/strict 210 | "strict": ["error", "safe"], 211 | 212 | /////////////////////////////////////////////////////////////////////////////////// 213 | // Variables https://eslint.org/docs/rules/#variables 214 | /////////////////////////////////////////////////////////////////////////////////// 215 | 216 | // https://eslint.org/docs/rules/no-shadow 217 | "no-shadow": ["error", { "builtinGlobals": true, "hoist": "all", "allow": [] }], 218 | 219 | // https://eslint.org/docs/rules/no-shadow-restricted-names 220 | "no-shadow-restricted-names": "error", 221 | 222 | // https://eslint.org/docs/rules/no-undef-init 223 | "no-undef-init": "error", 224 | 225 | // https://eslint.org/docs/rules/no-use-before-define 226 | "no-use-before-define": ["error", { "functions": true, "classes": true, "variables": true }], 227 | 228 | /////////////////////////////////////////////////////////////////////////////////// 229 | // Node.js and CommonJS https://eslint.org/docs/rules/#nodejs-and-commonjs 230 | /////////////////////////////////////////////////////////////////////////////////// 231 | 232 | // https://eslint.org/docs/rules/global-require 233 | "global-require": "error", 234 | 235 | // https://eslint.org/docs/rules/no-buffer-constructor 236 | "no-buffer-constructor": "error", 237 | 238 | // https://eslint.org/docs/rules/no-new-require 239 | "no-new-require": "error", 240 | 241 | // https://eslint.org/docs/rules/no-path-concat 242 | "no-path-concat": "error", 243 | 244 | /////////////////////////////////////////////////////////////////////////////////// 245 | // Stylistic Issues https://eslint.org/docs/rules/#stylistic-issues 246 | /////////////////////////////////////////////////////////////////////////////////// 247 | 248 | // https://eslint.org/docs/rules/array-bracket-newline 249 | "array-bracket-newline": ["warn", { "multiline": true }], 250 | 251 | // https://eslint.org/docs/rules/array-bracket-spacing 252 | "array-bracket-spacing": ["warn", "never"], 253 | 254 | // https://eslint.org/docs/rules/block-spacing 255 | "block-spacing": ["warn", "always"], 256 | 257 | // https://eslint.org/docs/rules/brace-style 258 | "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], 259 | 260 | // https://eslint.org/docs/rules/camelcase 261 | "camelcase": ["warn", { "properties": "always" }], 262 | 263 | // https://eslint.org/docs/rules/capitalized-comments 264 | "capitalized-comments": ["warn", "always", { "ignoreInlineComments": true, "ignoreConsecutiveComments": true }], 265 | 266 | // https://eslint.org/docs/rules/comma-dangle 267 | "comma-dangle": ["warn", "only-multiline"], 268 | 269 | // https://eslint.org/docs/rules/comma-spacing 270 | "comma-spacing": ["warn", { "before": false, "after": true }], 271 | 272 | // https://eslint.org/docs/rules/comma-style 273 | "comma-style": ["warn", "last"], 274 | 275 | // https://eslint.org/docs/rules/computed-property-spacing 276 | "computed-property-spacing": ["warn", "never"], 277 | 278 | // TODO - discuss with TEAM! 279 | // https://eslint.org/docs/rules/consistent-this 280 | "consistent-this": ["warn", "that", "_this", "self"], 281 | 282 | // https://eslint.org/docs/rules/eol-last 283 | "eol-last": ["warn", "always"], 284 | 285 | // https://eslint.org/docs/rules/func-call-spacing 286 | "func-call-spacing": ["warn", "never"], 287 | 288 | // https://eslint.org/docs/rules/func-name-matching 289 | "func-name-matching": ["warn", "always"], 290 | 291 | // https://eslint.org/docs/rules/implicit-arrow-linebreak 292 | "implicit-arrow-linebreak": ["warn", "beside"], 293 | 294 | // https://eslint.org/docs/rules/indent 295 | "indent": ["warn", 4, { 296 | "SwitchCase": 1, 297 | "FunctionDeclaration": { 298 | "parameters": 2, 299 | "body": 1 300 | }, 301 | "FunctionExpression": { 302 | "parameters": 2, 303 | "body": 1 304 | }, 305 | "CallExpression": { 306 | "arguments": 1 307 | }, 308 | "ArrayExpression": 1, 309 | "ObjectExpression": 1, 310 | "ImportDeclaration": 1, 311 | "ignoredNodes": [ 312 | "ConditionalExpression" 313 | ] 314 | }], 315 | 316 | // https://eslint.org/docs/rules/jsx-quotes 317 | "jsx-quotes": ["warn", "prefer-double"], 318 | 319 | // https://eslint.org/docs/rules/key-spacing 320 | "key-spacing": [ 321 | "warn", 322 | { 323 | "singleLine": { 324 | "beforeColon": false, 325 | "afterColon": true, 326 | "mode": "strict" 327 | }, 328 | "multiLine": { 329 | "beforeColon": false, 330 | "afterColon": true, 331 | "mode": "minimum" 332 | } 333 | } 334 | ], 335 | 336 | // https://eslint.org/docs/rules/keyword-spacing 337 | "keyword-spacing": ["warn", { "before": true, "after": true }], 338 | 339 | // https://eslint.org/docs/rules/linebreak-style 340 | "linebreak-style": ["warn", "unix"], 341 | 342 | // https://eslint.org/docs/rules/lines-around-comment 343 | "lines-around-comment": ["warn", { 344 | "beforeBlockComment": true, 345 | "afterBlockComment": false, 346 | "beforeLineComment": true, 347 | "afterLineComment": false, 348 | "allowBlockStart": true, 349 | "allowBlockEnd": false, 350 | "allowClassStart": true, 351 | "allowClassEnd": false, 352 | "allowObjectStart": true, 353 | "allowObjectEnd": false, 354 | "allowArrayStart": true, 355 | "allowArrayEnd": false 356 | }], 357 | 358 | // https://eslint.org/docs/rules/max-len 359 | "max-len": ["warn", { 360 | "code": 120, 361 | "tabWidth": 4, 362 | "ignoreUrls": true, 363 | "ignoreComments": true 364 | }], 365 | 366 | // https://eslint.org/docs/rules/max-lines 367 | "max-lines": ["warn", { 368 | "max": 500, 369 | "skipBlankLines": true, 370 | "skipComments": true 371 | }], 372 | 373 | // https://eslint.org/docs/rules/max-statements 374 | "max-statements": ["warn", 30], 375 | 376 | // https://eslint.org/docs/rules/max-statements-per-line 377 | "max-statements-per-line": ["warn", { 378 | "max": 1 379 | }], 380 | 381 | // https://eslint.org/docs/rules/multiline-ternary 382 | "multiline-ternary": ["warn", "always-multiline"], 383 | 384 | // https://eslint.org/docs/rules/new-cap 385 | "new-cap": ["warn", { "newIsCap": true, "capIsNew": true, "properties": true }], 386 | 387 | // https://eslint.org/docs/rules/new-parens 388 | "new-parens": "warn", 389 | 390 | // https://eslint.org/docs/rules/newline-per-chained-call 391 | "newline-per-chained-call": ["warn", { "ignoreChainWithDepth": 2 }], 392 | 393 | // https://eslint.org/docs/rules/no-array-constructor 394 | "no-array-constructor": "warn", 395 | 396 | // https://eslint.org/docs/rules/no-bitwise 397 | "no-bitwise": "warn", 398 | 399 | // https://eslint.org/docs/rules/no-lonely-if 400 | "no-lonely-if": "warn", 401 | 402 | // https://eslint.org/docs/rules/no-mixed-operators 403 | "no-mixed-operators": "warn", 404 | 405 | // https://eslint.org/docs/rules/no-multi-assign 406 | "no-multi-assign": "warn", 407 | 408 | // https://eslint.org/docs/rules/no-multiple-empty-lines 409 | "no-multiple-empty-lines": ["warn", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], 410 | 411 | // https://eslint.org/docs/rules/no-negated-condition 412 | "no-negated-condition": "warn", 413 | 414 | // https://eslint.org/docs/rules/no-new-object 415 | "no-new-object": "warn", 416 | 417 | // https://eslint.org/docs/rules/no-tabs 418 | "no-tabs": "warn", 419 | 420 | // https://eslint.org/docs/rules/no-trailing-spaces 421 | "no-trailing-spaces": ["warn", { "skipBlankLines": false, "ignoreComments": false }], 422 | 423 | // https://eslint.org/docs/rules/no-unneeded-ternary 424 | "no-unneeded-ternary": ["warn", { "defaultAssignment": false }], 425 | 426 | // https://eslint.org/docs/rules/no-whitespace-before-property 427 | "no-whitespace-before-property": "warn", 428 | 429 | // https://eslint.org/docs/rules/object-curly-newline 430 | "object-curly-newline": ["warn", { "consistent": true }], 431 | 432 | // https://eslint.org/docs/rules/object-curly-spacing 433 | "object-curly-spacing": ["warn", "always"], 434 | 435 | // https://eslint.org/docs/rules/one-var 436 | "one-var": ["error", "never"], 437 | 438 | // https://eslint.org/docs/rules/operator-linebreak 439 | "operator-linebreak": ["warn", "after"], 440 | 441 | // https://eslint.org/docs/rules/padded-blocks 442 | "padded-blocks": ["warn", "never"], 443 | 444 | // https://eslint.org/docs/rules/quote-props 445 | "quote-props": ["warn", "as-needed"], 446 | 447 | // https://eslint.org/docs/rules/quotes 448 | "quotes": ["warn", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 449 | 450 | // https://eslint.org/docs/rules/semi 451 | "semi": ["warn", "always"], 452 | 453 | // https://eslint.org/docs/rules/semi-spacing 454 | "semi-spacing": ["warn", { "before": false, "after": true }], 455 | 456 | // https://eslint.org/docs/rules/semi-style 457 | "semi-style": ["warn", "last"], 458 | 459 | // https://eslint.org/docs/rules/space-before-blocks 460 | "space-before-blocks": ["warn", "always"], 461 | 462 | // https://eslint.org/docs/rules/space-before-function-paren 463 | "space-before-function-paren": ["warn", "never"], 464 | 465 | // https://eslint.org/docs/rules/space-in-parens 466 | "space-in-parens": ["warn", "never"], 467 | 468 | // https://eslint.org/docs/rules/space-infix-ops 469 | "space-infix-ops": "warn", 470 | 471 | // https://eslint.org/docs/rules/space-unary-ops 472 | "space-unary-ops": ["warn", { "words": true, "nonwords": false }], 473 | 474 | // https://eslint.org/docs/rules/spaced-comment 475 | "spaced-comment": ["warn", "always", { "exceptions": ["-", "/", "=", "*"] }], 476 | 477 | // https://eslint.org/docs/rules/switch-colon-spacing 478 | "switch-colon-spacing": ["warn", { "after": true, "before": false }], 479 | 480 | // https://eslint.org/docs/rules/unicode-bom 481 | "unicode-bom": ["error", "never"], 482 | 483 | /////////////////////////////////////////////////////////////////////////////////// 484 | // ECMAScript 6 https://eslint.org/docs/rules/#ecmascript-6 485 | /////////////////////////////////////////////////////////////////////////////////// 486 | 487 | // https://eslint.org/docs/rules/arrow-spacing 488 | "arrow-spacing": ["warn", { "before": true, "after": true }] 489 | 490 | // TODO - more ES6 rules 491 | }, 492 | "parserOptions": { 493 | "sourceType": "module" 494 | } 495 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ### Is this a bug report, a feature request, or a question? 15 | 16 | (Write your answer here.) 17 | 18 | 30 | 31 | ### Have you followed the required steps before opening a bug report? 32 | 33 | (Check the step you've followed - put an `x` character between the square brackets (`[]`).) 34 | 35 | - [] I have reviewed [the documentation](https://github.com/EkoLabs/react-native-background-downloader/blob/master/README.md) in its entirety, including the dedicated documentations :books:. 36 | - [] I have searched for [existing issues](https://github.com/EkoLabs/react-native-background-downloader/issues) and made sure that the problem hasn't already been reported. 37 | - [] I am using [the latest plugin version](https://github.com/EkoLabs/react-native-background-downloader/releases). 38 | 39 | 43 | 44 | ### Is the bug specific to iOS or Android? Or can it be reproduced on both platforms? 45 | 46 | (Write your answer here and specify the iOS/Android versions on which you've been able to reproduce the issue.) 47 | 48 | ### Is the bug related to the native implementation? (NSURLSession on iOS and Fetch on Android) 49 | 53 | 54 | (Write your answer here.) 55 | 56 | ### Environment 57 | 58 | 70 | 71 | (Write your answer here.) 72 | 73 | ### Expected Behavior 74 | 75 | 80 | 81 | (Write what you thought would happen.) 82 | 83 | ### Actual Behavior 84 | 85 | 91 | 92 | (Write what happened. Add logs!) 93 | 94 | ### Steps to Reproduce 95 | 96 | 101 | 102 | (Write your steps so that anyone can reproduce the issue in the Snack demo you provided.) 103 | 104 | 1. 105 | 2. 106 | 3. 107 | 108 | 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2018 Eko 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![react-native-background-downloader banner](https://d1w2zhnqcy4l8f.cloudfront.net/content/falcon/production/projects/V5EEOX_fast/RNBD-190702083358.png) 2 | 3 | [![npm version](https://badge.fury.io/js/react-native-background-downloader.svg)](https://badge.fury.io/js/react-native-background-downloader) 4 | 5 | # react-native-background-downloader 6 | 7 | ## This repo is no longer actively maintained by eko, however you may be interested in checking out this [fork](https://github.com/kesha-antonov/react-native-background-downloader) ## 8 | 9 | A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background. 10 | 11 | ### Why? 12 | On iOS, if you want to download big files no matter the state of your app, wether it's in the background or terminated by the OS, you have to use a system API called `NSURLSession`. 13 | 14 | This API handles your downloads separately from your app and only keeps it informed using delegates (Read: [Downloading Files in the Background](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background)). 15 | 16 | On Android we are simulating this process with a wonderful library called [Fetch2](https://github.com/tonyofrancis/Fetch) 17 | 18 | The real challenge of using this method is making sure the app's UI is always up-to-date with the downloads that are happening in another process because your app might startup from scratch while the downloads are still running. 19 | 20 | `react-native-background-downloader` gives you an easy API to both downloading large files and re-attaching to those downloads once your app launches again. 21 | 22 | > **Please Note** - This library was created to better facilitate background downloading on iOS. If you're not aiming to to use the download-in-background functionality, there are better solutions like [RNFS.downloadFile()](https://github.com/itinance/react-native-fs#downloadfileoptions-downloadfileoptions--jobid-number-promise-promisedownloadresult-) which will results in a more stable download experience for your app. 23 | 24 | ## ToC 25 | 26 | - [Usage](#usage) 27 | - [API](#api) 28 | - [Constants](#constants) 29 | 30 | ## Getting started 31 | 32 | `$ yarn add react-native-background-downloader` 33 | 34 | For **`RN <= 0.57.0`** use `$ yarn add react-native-background-downloader@1.1.0` 35 | 36 | ### Mostly automatic installation 37 | Any React Native version **`>= 0.60`** supports autolinking so nothing should be done. 38 | 39 | For anything **`< 0.60`** run the following link command 40 | 41 | `$ react-native link react-native-background-downloader` 42 | 43 | ### Manual installation 44 | 45 | 46 | #### iOS 47 | 48 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 49 | 2. Go to `node_modules` ➜ `react-native-background-downloader` and add `RNBackgroundDownloader.xcodeproj` 50 | 3. In XCode, in the project navigator, select your project. Add `libRNBackgroundDownloader.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 51 | 4. Run your project (`Cmd+R`) 52 | 53 | #### Android 54 | 55 | 1. Open up `android/app/src/main/java/[...]/MainActivity.java` 56 | - Add `import com.eko.RNBackgroundDownloaderPackage;` to the imports at the top of the file 57 | - Add `new RNBackgroundDownloaderPackage()` to the list returned by the `getPackages()` method 58 | 2. Append the following lines to `android/settings.gradle`: 59 | ``` 60 | include ':react-native-background-downloader' 61 | project(':react-native-background-downloader').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-downloader/android') 62 | ``` 63 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 64 | ``` 65 | compile project(':react-native-background-downloader') 66 | ``` 67 | 68 | ### iOS - Extra Mandatory Step 69 | In your `AppDelegate.m` add the following code: 70 | ```objc 71 | ... 72 | #import 73 | 74 | ... 75 | 76 | - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler 77 | { 78 | [RNBackgroundDownloader setCompletionHandlerWithIdentifier:identifier completionHandler:completionHandler]; 79 | } 80 | 81 | ... 82 | ``` 83 | Failing to add this code will result in canceled background downloads. 84 | 85 | ## Usage 86 | 87 | ### Downloading a file 88 | 89 | ```javascript 90 | import RNBackgroundDownloader from 'react-native-background-downloader'; 91 | 92 | let task = RNBackgroundDownloader.download({ 93 | id: 'file123', 94 | url: 'https://link-to-very.large/file.zip' 95 | destination: `${RNBackgroundDownloader.directories.documents}/file.zip` 96 | }).begin((expectedBytes) => { 97 | console.log(`Going to download ${expectedBytes} bytes!`); 98 | }).progress((percent) => { 99 | console.log(`Downloaded: ${percent * 100}%`); 100 | }).done(() => { 101 | console.log('Download is done!'); 102 | }).error((error) => { 103 | console.log('Download canceled due to error: ', error); 104 | }); 105 | 106 | // Pause the task 107 | task.pause(); 108 | 109 | // Resume after pause 110 | task.resume(); 111 | 112 | // Cancel the task 113 | task.stop(); 114 | ``` 115 | 116 | ### Re-Attaching to background downloads 117 | 118 | This is the main selling point of this library (but it's free!). 119 | 120 | What happens to your downloads after the OS stopped your app? Well, they are still running, we just need to re-attach to them. 121 | 122 | Add this code to app's init stage, and you'll never lose a download again! 123 | 124 | ```javascript 125 | import RNBackgroundDownloader from 'react-native-background-downloader'; 126 | 127 | let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); 128 | for (let task of lostTasks) { 129 | console.log(`Task ${task.id} was found!`); 130 | task.progress((percent) => { 131 | console.log(`Downloaded: ${percent * 100}%`); 132 | }).done(() => { 133 | console.log('Downlaod is done!'); 134 | }).error((error) => { 135 | console.log('Download canceled due to error: ', error); 136 | }); 137 | } 138 | ``` 139 | 140 | `task.id` is very important for re-attaching the download task with any UI component representing that task, this is why you need to make sure to give sensible IDs that you know what to do with, try to avoid using random IDs. 141 | 142 | ### Using custom headers 143 | If you need to send custom headers with your download request, you can do in it 2 ways: 144 | 145 | 1) Globally using `RNBackgroundDownloader.setHeaders()`: 146 | ```javascript 147 | RNBackgroundDownloader.setHeaders({ 148 | Authorization: 'Bearer 2we$@$@Ddd223' 149 | }); 150 | ``` 151 | This way, all downloads with have the given headers. 152 | 153 | 2) Per download by passing a headers object in the options of `RNBackgroundDownloader.download()`: 154 | ```javascript 155 | let task = RNBackgroundDownloader.download({ 156 | id: 'file123', 157 | url: 'https://link-to-very.large/file.zip' 158 | destination: `${RNBackgroundDownloader.directories.documents}/file.zip`, 159 | headers: { 160 | Authorization: 'Bearer 2we$@$@Ddd223' 161 | } 162 | }).begin((expectedBytes) => { 163 | console.log(`Going to download ${expectedBytes} bytes!`); 164 | }).progress((percent) => { 165 | console.log(`Downloaded: ${percent * 100}%`); 166 | }).done(() => { 167 | console.log('Download is done!'); 168 | }).error((error) => { 169 | console.log('Download canceled due to error: ', error); 170 | }); 171 | ``` 172 | Headers given in the `download` function are **merged** with the ones given in `setHeaders`. 173 | 174 | ## API 175 | 176 | ### RNBackgroundDownloader 177 | 178 | ### `download(options)` 179 | 180 | Download a file to destination 181 | 182 | **options** 183 | 184 | An object containing options properties 185 | 186 | | Property | Type | Required | Platforms | Info | 187 | | ------------- | ------------------------------------------------ | :------: | :-------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 188 | | `id` | String | ✅ | All | A Unique ID to provide for this download. This ID will help to identify the download task when the app re-launches | 189 | | `url` | String | ✅ | All | URL to file you want to download | 190 | | `destination` | String | ✅ | All | Where to copy the file to once the download is done | 191 | | `headers` | Object | | All | Costume headers to add to the download request. These are merged with the headers given in the `setHeaders` function 192 | | `priority` | [Priority (enum)](#priority-enum---android-only) | | Android | The priority of the download. On Android, downloading is limited to 4 simultaneous instances where further downloads are queued. Priority helps in deciding which download to pick next from the queue. **Default:** Priority.MEDIUM | 193 | | `network` | [Network (enum)](#network-enum---android-only) | | Android | Give your the ability to limit the download to WIFI only. **Default:** Network.ALL | 194 | 195 | **returns** 196 | 197 | `DownloadTask` - The download task to control and monitor this download 198 | 199 | ### `checkForExistingDownloads()` 200 | 201 | Checks for downloads that ran in background while you app was terminated. Recommended to run at the init stage of the app. 202 | 203 | **returns** 204 | 205 | `DownloadTask[]` - Array of tasks that were running in the background so you can re-attach callbacks to them 206 | 207 | ### `setHeaders(headers)` 208 | 209 | Sets headers to use in all future downloads. 210 | 211 | **headers** - Object 212 | 213 | ### DownloadTask 214 | 215 | A class representing a download task created by `RNBackgroundDownloader.download` 216 | 217 | ### `Members` 218 | | Name | Type | Info | 219 | | -------------- | ------ | ---------------------------------------------------------------------------------------------------- | 220 | | `id` | String | The id you gave the task when calling `RNBackgroundDownloader.download` | 221 | | `percent` | Number | The current percent of completion of the task between 0 and 1 | 222 | | `bytesWritten` | Number | The number of bytes currently written by the task | 223 | | `totalBytes` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded | 224 | 225 | ### `Callback Methods` 226 | Use these methods to stay updated on what's happening with the task. 227 | 228 | All callback methods return the current instance of the `DownloadTask` for chaining. 229 | 230 | | Function | Callback Arguments | Info | 231 | | ---------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | 232 | | `begin` | expectedBytes | Called when the first byte is received. 💡: this is good place to check if the device has enough storage space for this download | 233 | | `progress` | percent, bytesWritten, totalBytes | Called at max every 1.5s so you can update your progress bar accordingly | 234 | | `done` | | Called when the download is done, the file is at the destination you've set | 235 | | `error` | error | Called when the download stops due to an error | 236 | 237 | ### `pause()` 238 | Pauses the download 239 | 240 | ### `resume()` 241 | Resumes a pause download 242 | 243 | ### `stop()` 244 | Stops the download for good and removes the file that was written so far 245 | 246 | ## Constants 247 | 248 | ### directories 249 | 250 | ### `documents` 251 | 252 | An absolute path to the app's documents directory. It is recommended that you use this path as the target of downloaded files. 253 | 254 | ### Priority (enum) - Android only 255 | 256 | `Priority.HIGH` 257 | 258 | `Priority.MEDIUM` - Default ✅ 259 | 260 | `Priority.LOW` 261 | 262 | ### Network (enum) - Android only 263 | 264 | `Network.WIFI_ONLY` 265 | 266 | `Network.ALL` - Default ✅ 267 | 268 | ## Author 269 | Developed by [Elad Gil](https://github.com/ptelad) of [Eko](http://www.helloeko.com) 270 | 271 | ## License 272 | Apache 2 273 | -------------------------------------------------------------------------------- /__mocks__/RNBackgroundDownloader.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { NativeModules } from 'react-native'; 4 | 5 | // states: 6 | // 0 - Running 7 | // 1 - Suspended / Paused 8 | // 2 - Cancelled / Failed 9 | // 3 - Completed (not necessarily successfully) 10 | 11 | NativeModules.RNBackgroundDownloader = { 12 | download: jest.fn(), 13 | pauseTask: jest.fn(), 14 | resumeTask: jest.fn(), 15 | stopTask: jest.fn(), 16 | TaskRunning: 0, 17 | TaskSuspended: 1, 18 | TaskCanceling: 2, 19 | TaskCompleted: 3, 20 | checkForExistingDownloads: jest.fn().mockImplementation(() => { 21 | foundDownloads = [ 22 | { 23 | id: 'taskRunning', 24 | state: NativeModules.RNBackgroundDownloader.TaskRunning, 25 | percent: 0.5, 26 | bytesWritten: 50, 27 | totalBytes: 100 28 | }, 29 | { 30 | id: 'taskPaused', 31 | state: NativeModules.RNBackgroundDownloader.TaskSuspended, 32 | percent: 0.7, 33 | bytesWritten: 70, 34 | totalBytes: 100 35 | }, 36 | { 37 | id: 'taskCancelled', 38 | percent: 0.9, 39 | state: NativeModules.RNBackgroundDownloader.TaskCanceling, 40 | bytesWritten: 90, 41 | totalBytes: 100 42 | }, 43 | { 44 | id: 'taskCompletedExplicit', 45 | state: NativeModules.RNBackgroundDownloader.TaskCompleted, 46 | percent: 1, 47 | bytesWritten: 100, 48 | totalBytes: 100 49 | }, 50 | { 51 | id: 'taskCompletedImplicit', 52 | state: NativeModules.RNBackgroundDownloader.TaskCompleted, 53 | percent: 1, 54 | bytesWritten: 100, 55 | totalBytes: 100 56 | }, 57 | { 58 | id: 'taskFailed', 59 | state: NativeModules.RNBackgroundDownloader.TaskCompleted, 60 | percent: 0.9, 61 | bytesWritten: 90, 62 | totalBytes: 100 63 | } 64 | ] 65 | return Promise.resolve(foundDownloads); 66 | }) 67 | }; -------------------------------------------------------------------------------- /__tests__/mainTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | jest.mock('NativeEventEmitter', () => { 4 | return class NativeEventEmitter { 5 | static listeners = {}; 6 | 7 | addListener(channel, cb) { 8 | NativeEventEmitter.listeners[channel] = cb; 9 | } 10 | }; 11 | }); 12 | 13 | import RNBackgroundDownloader from '../index'; 14 | import DownloadTask from '../lib/downloadTask'; 15 | import { NativeEventEmitter, NativeModules } from 'react-native'; 16 | 17 | const RNBackgroundDownloaderNative = NativeModules.RNBackgroundDownloader; 18 | 19 | let downloadTask; 20 | 21 | test('download function', () => { 22 | downloadTask = RNBackgroundDownloader.download({ 23 | id: 'test', 24 | url: 'test', 25 | destination: 'test' 26 | }); 27 | expect(downloadTask).toBeInstanceOf(DownloadTask); 28 | expect(RNBackgroundDownloaderNative.download).toHaveBeenCalled(); 29 | }); 30 | 31 | test('begin event', () => { 32 | return new Promise(resolve => { 33 | const beginDT = RNBackgroundDownloader.download({ 34 | id: 'testBegin', 35 | url: 'test', 36 | destination: 'test' 37 | }).begin((expectedBytes) => { 38 | expect(expectedBytes).toBe(9001); 39 | expect(beginDT.state).toBe('DOWNLOADING'); 40 | resolve(); 41 | }); 42 | NativeEventEmitter.listeners.downloadBegin({ 43 | id: 'testBegin', 44 | expectedBytes: 9001 45 | }); 46 | }); 47 | }); 48 | 49 | test('progress event', () => { 50 | return new Promise(resolve => { 51 | RNBackgroundDownloader.download({ 52 | id: 'testProgress', 53 | url: 'test', 54 | destination: 'test' 55 | }).progress((percent, bytesWritten, totalBytes) => { 56 | expect(percent).toBeCloseTo(0.7); 57 | expect(bytesWritten).toBe(100); 58 | expect(totalBytes).toBe(200); 59 | resolve(); 60 | }); 61 | NativeEventEmitter.listeners.downloadProgress([{ 62 | id: 'testProgress', 63 | percent: 0.7, 64 | written: 100, 65 | total: 200 66 | }]); 67 | }); 68 | }); 69 | 70 | test('done event', () => { 71 | return new Promise(resolve => { 72 | const doneDT = RNBackgroundDownloader.download({ 73 | id: 'testDone', 74 | url: 'test', 75 | destination: 'test' 76 | }).done(() => { 77 | expect(doneDT.state).toBe('DONE'); 78 | resolve(); 79 | }); 80 | NativeEventEmitter.listeners.downloadComplete({ 81 | id: 'testDone' 82 | }); 83 | }); 84 | }); 85 | 86 | test('fail event', () => { 87 | return new Promise(resolve => { 88 | const failDT = RNBackgroundDownloader.download({ 89 | id: 'testFail', 90 | url: 'test', 91 | destination: 'test' 92 | }).error((error) => { 93 | expect(error).toBeInstanceOf(Error); 94 | expect(failDT.state).toBe('FAILED'); 95 | resolve(); 96 | }); 97 | NativeEventEmitter.listeners.downloadFailed({ 98 | id: 'testFail', 99 | error: new Error('test') 100 | }); 101 | }); 102 | }); 103 | 104 | test('pause', () => { 105 | const pauseDT = RNBackgroundDownloader.download({ 106 | id: 'testPause', 107 | url: 'test', 108 | destination: 'test' 109 | }); 110 | 111 | pauseDT.pause(); 112 | expect(pauseDT.state).toBe('PAUSED'); 113 | expect(RNBackgroundDownloaderNative.pauseTask).toHaveBeenCalled(); 114 | }); 115 | 116 | test('resume', () => { 117 | const resumeDT = RNBackgroundDownloader.download({ 118 | id: 'testResume', 119 | url: 'test', 120 | destination: 'test' 121 | }); 122 | 123 | resumeDT.resume(); 124 | expect(resumeDT.state).toBe('DOWNLOADING'); 125 | expect(RNBackgroundDownloaderNative.resumeTask).toHaveBeenCalled(); 126 | }); 127 | 128 | test('stop', () => { 129 | const stopDT = RNBackgroundDownloader.download({ 130 | id: 'testStop', 131 | url: 'test', 132 | destination: 'test' 133 | }); 134 | 135 | stopDT.stop(); 136 | expect(stopDT.state).toBe('STOPPED'); 137 | expect(RNBackgroundDownloaderNative.stopTask).toHaveBeenCalled(); 138 | }); 139 | 140 | test('checkForExistingDownloads', () => { 141 | return RNBackgroundDownloader.checkForExistingDownloads() 142 | .then(foundDownloads => { 143 | expect(RNBackgroundDownloaderNative.checkForExistingDownloads).toHaveBeenCalled(); 144 | expect(foundDownloads.length).toBe(4); 145 | foundDownloads.forEach(foundDownload => { 146 | expect(foundDownload).toBeInstanceOf(DownloadTask); 147 | expect(foundDownload.state).not.toBe('FAILED'); 148 | expect(foundDownload.state).not.toBe('STOPPED'); 149 | }); 150 | }) 151 | }); 152 | 153 | test('wrong handler type', () => { 154 | let dt = RNBackgroundDownloader.download({ 155 | id: 'test22222', 156 | url: 'test', 157 | destination: 'test' 158 | }); 159 | 160 | expect(() => { 161 | dt.begin('not function'); 162 | }).toThrow(); 163 | 164 | expect(() => { 165 | dt.progress(7); 166 | }).toThrow(); 167 | 168 | expect(() => { 169 | dt.done({iamnota: 'function'}); 170 | }).toThrow(); 171 | 172 | expect(() => { 173 | dt.error('not function'); 174 | }).toThrow(); 175 | }); -------------------------------------------------------------------------------- /android/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-native-background-downloader 4 | Project react-native-background-downloader created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def safeExtGet(prop, fallback) { 4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 5 | } 6 | 7 | android { 8 | compileSdkVersion safeExtGet("compileSdkVersion", 28) 9 | 10 | defaultConfig { 11 | minSdkVersion safeExtGet('minSdkVersion', 16) 12 | targetSdkVersion safeExtGet('targetSdkVersion', 28) 13 | versionCode 1 14 | versionName "1.0" 15 | ndk { 16 | abiFilters "armeabi-v7a", "x86" 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | //noinspection GradleDynamicVersion 23 | implementation 'com.facebook.react:react-native:+' 24 | if (project.properties['android.useAndroidX'] == 'true' || project.properties['android.useAndroidX'] == true) { 25 | api "androidx.tonyodev.fetch2:xfetch2:3.1.4" 26 | } else { 27 | api "com.tonyodev.fetch2:fetch2:3.0.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/react-native-background-downloader/2bf7da520b2877cb0e20d8e2c56356b330c34f6e/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jan 20 11:41:54 IST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/eko/RNBGDTaskConfig.java: -------------------------------------------------------------------------------- 1 | package com.eko; 2 | 3 | import java.io.Serializable; 4 | 5 | public class RNBGDTaskConfig implements Serializable { 6 | public String id; 7 | public boolean reportedBegin; 8 | 9 | public RNBGDTaskConfig(String id) { 10 | this.id = id; 11 | this.reportedBegin = false; 12 | } 13 | } -------------------------------------------------------------------------------- /android/src/main/java/com/eko/RNBackgroundDownloaderModule.java: -------------------------------------------------------------------------------- 1 | package com.eko; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.bridge.Arguments; 7 | import com.facebook.react.bridge.Promise; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 10 | import com.facebook.react.bridge.ReactMethod; 11 | import com.facebook.react.bridge.ReadableMap; 12 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 13 | import com.facebook.react.bridge.WritableArray; 14 | import com.facebook.react.bridge.WritableMap; 15 | import com.facebook.react.modules.core.DeviceEventManagerModule; 16 | import com.tonyodev.fetch2.Download; 17 | import com.tonyodev.fetch2.Error; 18 | import com.tonyodev.fetch2.Fetch; 19 | import com.tonyodev.fetch2.FetchConfiguration; 20 | import com.tonyodev.fetch2.FetchListener; 21 | import com.tonyodev.fetch2.NetworkType; 22 | import com.tonyodev.fetch2.Priority; 23 | import com.tonyodev.fetch2.Request; 24 | import com.tonyodev.fetch2.Status; 25 | import com.tonyodev.fetch2core.DownloadBlock; 26 | import com.tonyodev.fetch2core.Func; 27 | 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.io.File; 31 | import java.io.FileInputStream; 32 | import java.io.FileOutputStream; 33 | import java.io.IOException; 34 | import java.io.ObjectInputStream; 35 | import java.io.ObjectOutputStream; 36 | import java.util.Date; 37 | import java.util.HashMap; 38 | import java.util.List; 39 | import java.util.Map; 40 | 41 | import javax.annotation.Nullable; 42 | 43 | public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule implements FetchListener { 44 | 45 | private static final int TASK_RUNNING = 0; 46 | private static final int TASK_SUSPENDED = 1; 47 | private static final int TASK_CANCELING = 2; 48 | private static final int TASK_COMPLETED = 3; 49 | 50 | private static final int ERR_STORAGE_FULL = 0; 51 | private static final int ERR_NO_INTERNET = 1; 52 | private static final int ERR_NO_WRITE_PERMISSION = 2; 53 | private static final int ERR_FILE_NOT_FOUND = 3; 54 | private static final int ERR_OTHERS = 100; 55 | 56 | private static Map stateMap = new HashMap() {{ 57 | put(Status.DOWNLOADING, TASK_RUNNING); 58 | put(Status.COMPLETED, TASK_COMPLETED); 59 | put(Status.PAUSED, TASK_SUSPENDED); 60 | put(Status.QUEUED, TASK_RUNNING); 61 | put(Status.CANCELLED, TASK_CANCELING); 62 | put(Status.FAILED, TASK_CANCELING); 63 | put(Status.REMOVED, TASK_CANCELING); 64 | put(Status.DELETED, TASK_CANCELING); 65 | put(Status.NONE, TASK_CANCELING); 66 | }}; 67 | 68 | private Fetch fetch; 69 | private Map idToRequestId = new HashMap<>(); 70 | @SuppressLint("UseSparseArrays") 71 | private Map requestIdToConfig = new HashMap<>(); 72 | private DeviceEventManagerModule.RCTDeviceEventEmitter ee; 73 | private Date lastProgressReport = new Date(); 74 | private HashMap progressReports = new HashMap<>(); 75 | private static Object sharedLock = new Object(); 76 | 77 | public RNBackgroundDownloaderModule(ReactApplicationContext reactContext) { 78 | super(reactContext); 79 | 80 | loadConfigMap(); 81 | FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(this.getReactApplicationContext()) 82 | .setDownloadConcurrentLimit(4) 83 | .setNamespace("RNBackgroundDownloader") 84 | .build(); 85 | fetch = Fetch.Impl.getInstance(fetchConfiguration); 86 | fetch.addListener(this); 87 | } 88 | 89 | @Override 90 | public void onCatalystInstanceDestroy() { 91 | fetch.close(); 92 | } 93 | 94 | @Override 95 | public String getName() { 96 | return "RNBackgroundDownloader"; 97 | } 98 | 99 | @Override 100 | public void initialize() { 101 | ee = getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); 102 | } 103 | 104 | @Override 105 | public boolean hasConstants() { 106 | return true; 107 | } 108 | 109 | @Nullable 110 | @Override 111 | public Map getConstants() { 112 | Map constants = new HashMap<>(); 113 | File externalDirectory = this.getReactApplicationContext().getExternalFilesDir(null); 114 | if (externalDirectory != null) { 115 | constants.put("documents", externalDirectory.getAbsolutePath()); 116 | } else { 117 | constants.put("documents", this.getReactApplicationContext().getFilesDir().getAbsolutePath()); 118 | } 119 | 120 | constants.put("TaskRunning", TASK_RUNNING); 121 | constants.put("TaskSuspended", TASK_SUSPENDED); 122 | constants.put("TaskCanceling", TASK_CANCELING); 123 | constants.put("TaskCompleted", TASK_COMPLETED); 124 | constants.put("PriorityHigh", Priority.HIGH.getValue()); 125 | constants.put("PriorityNormal", Priority.NORMAL.getValue()); 126 | constants.put("PriorityLow", Priority.LOW.getValue()); 127 | constants.put("OnlyWifi", NetworkType.WIFI_ONLY.getValue()); 128 | constants.put("AllNetworks", NetworkType.ALL.getValue()); 129 | return constants; 130 | } 131 | 132 | private void removeFromMaps(int requestId) { 133 | synchronized(sharedLock) { 134 | RNBGDTaskConfig config = requestIdToConfig.get(requestId); 135 | if (config != null) { 136 | idToRequestId.remove(config.id); 137 | requestIdToConfig.remove(requestId); 138 | 139 | saveConfigMap(); 140 | } 141 | } 142 | } 143 | 144 | private void saveConfigMap() { 145 | synchronized(sharedLock) { 146 | File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); 147 | try { 148 | ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); 149 | outputStream.writeObject(requestIdToConfig); 150 | outputStream.flush(); 151 | outputStream.close(); 152 | } catch (IOException e) { 153 | e.printStackTrace(); 154 | } 155 | } 156 | } 157 | 158 | private void loadConfigMap() { 159 | File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); 160 | try { 161 | ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); 162 | requestIdToConfig = (Map) inputStream.readObject(); 163 | } catch (IOException | ClassNotFoundException e) { 164 | e.printStackTrace(); 165 | } 166 | } 167 | 168 | private int convertErrorCode(Error error) { 169 | if ((error == Error.FILE_NOT_CREATED) 170 | || (error == Error.WRITE_PERMISSION_DENIED)) { 171 | return ERR_NO_WRITE_PERMISSION; 172 | } else if ((error == Error.CONNECTION_TIMED_OUT) 173 | || (error == Error.NO_NETWORK_CONNECTION)) { 174 | return ERR_NO_INTERNET; 175 | } else if (error == Error.NO_STORAGE_SPACE) { 176 | return ERR_STORAGE_FULL; 177 | } else if (error == Error.FILE_NOT_FOUND) { 178 | return ERR_FILE_NOT_FOUND; 179 | } else { 180 | return ERR_OTHERS; 181 | } 182 | } 183 | 184 | // JS Methods 185 | @ReactMethod 186 | public void download(ReadableMap options) { 187 | final String id = options.getString("id"); 188 | String url = options.getString("url"); 189 | String destination = options.getString("destination"); 190 | ReadableMap headers = options.getMap("headers"); 191 | 192 | if (id == null || url == null || destination == null) { 193 | Log.e(getName(), "id, url and destination must be set"); 194 | return; 195 | } 196 | 197 | RNBGDTaskConfig config = new RNBGDTaskConfig(id); 198 | final Request request = new Request(url, destination); 199 | if (headers != null) { 200 | ReadableMapKeySetIterator it = headers.keySetIterator(); 201 | while (it.hasNextKey()) { 202 | String headerKey = it.nextKey(); 203 | request.addHeader(headerKey, headers.getString(headerKey)); 204 | } 205 | } 206 | request.setPriority(options.hasKey("priority") ? Priority.valueOf(options.getInt("priority")) : Priority.NORMAL); 207 | request.setNetworkType(options.hasKey("network") ? NetworkType.valueOf(options.getInt("network")) : NetworkType.ALL); 208 | 209 | fetch.enqueue(request, new Func() { 210 | @Override 211 | public void call(Request download) { 212 | } 213 | }, new Func() { 214 | @Override 215 | public void call(Error error) { 216 | //An error occurred when enqueuing a request. 217 | 218 | WritableMap params = Arguments.createMap(); 219 | params.putString("id", id); 220 | params.putString("error", error.toString()); 221 | 222 | int convertedErrCode = convertErrorCode(error); 223 | params.putInt("errorcode", convertedErrCode); 224 | ee.emit("downloadFailed", params); 225 | 226 | removeFromMaps(request.getId()); 227 | fetch.remove(request.getId()); 228 | 229 | Log.e(getName(), "Error in enqueue: " + error.toString() + ":" + error.getValue()); 230 | } 231 | } 232 | ); 233 | 234 | synchronized(sharedLock) { 235 | idToRequestId.put(id, request.getId()); 236 | requestIdToConfig.put(request.getId(), config); 237 | saveConfigMap(); 238 | } 239 | } 240 | 241 | @ReactMethod 242 | public void pauseTask(String identifier) { 243 | synchronized(sharedLock) { 244 | Integer requestId = idToRequestId.get(identifier); 245 | if (requestId != null) { 246 | fetch.pause(requestId); 247 | } 248 | } 249 | } 250 | 251 | @ReactMethod 252 | public void resumeTask(String identifier) { 253 | synchronized(sharedLock) { 254 | Integer requestId = idToRequestId.get(identifier); 255 | if (requestId != null) { 256 | fetch.resume(requestId); 257 | } 258 | } 259 | } 260 | 261 | @ReactMethod 262 | public void stopTask(String identifier) { 263 | synchronized(sharedLock) { 264 | Integer requestId = idToRequestId.get(identifier); 265 | if (requestId != null) { 266 | fetch.cancel(requestId); 267 | } 268 | } 269 | } 270 | 271 | @ReactMethod 272 | public void checkForExistingDownloads(final Promise promise) { 273 | fetch.getDownloads(new Func>() { 274 | @Override 275 | public void call(@NotNull List downloads) { 276 | WritableArray foundIds = Arguments.createArray(); 277 | 278 | synchronized(sharedLock) { 279 | for (Download download : downloads) { 280 | if (requestIdToConfig.containsKey(download.getId())) { 281 | RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); 282 | WritableMap params = Arguments.createMap(); 283 | params.putString("id", config.id); 284 | params.putInt("state", stateMap.get(download.getStatus())); 285 | params.putInt("bytesWritten", (int)download.getDownloaded()); 286 | params.putInt("totalBytes", (int)download.getTotal()); 287 | params.putDouble("percent", ((double)download.getProgress()) / 100); 288 | 289 | foundIds.pushMap(params); 290 | 291 | idToRequestId.put(config.id, download.getId()); 292 | config.reportedBegin = true; 293 | } else { 294 | fetch.delete(download.getId()); 295 | } 296 | } 297 | } 298 | 299 | promise.resolve(foundIds); 300 | } 301 | }); 302 | } 303 | 304 | // Fetch API 305 | @Override 306 | public void onCompleted(Download download) { 307 | synchronized(sharedLock) { 308 | RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); 309 | if (config != null) { 310 | WritableMap params = Arguments.createMap(); 311 | params.putString("id", config.id); 312 | ee.emit("downloadComplete", params); 313 | } 314 | 315 | removeFromMaps(download.getId()); 316 | if (!fetch.isClosed()) { 317 | fetch.remove(download.getId()); 318 | } 319 | } 320 | } 321 | 322 | @Override 323 | public void onProgress(Download download, long l, long l1) { 324 | synchronized(sharedLock) { 325 | RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); 326 | if (config == null) { 327 | return; 328 | } 329 | 330 | WritableMap params = Arguments.createMap(); 331 | params.putString("id", config.id); 332 | 333 | if (!config.reportedBegin) { 334 | params.putInt("expectedBytes", (int)download.getTotal()); 335 | ee.emit("downloadBegin", params); 336 | config.reportedBegin = true; 337 | } else { 338 | params.putInt("written", (int)download.getDownloaded()); 339 | params.putInt("total", (int)download.getTotal()); 340 | params.putDouble("percent", ((double)download.getProgress()) / 100); 341 | progressReports.put(config.id, params); 342 | Date now = new Date(); 343 | if (now.getTime() - lastProgressReport.getTime() > 1500) { 344 | WritableArray reportsArray = Arguments.createArray(); 345 | for (WritableMap report : progressReports.values()) { 346 | reportsArray.pushMap(report); 347 | } 348 | ee.emit("downloadProgress", reportsArray); 349 | lastProgressReport = now; 350 | progressReports.clear(); 351 | } 352 | } 353 | } 354 | } 355 | 356 | @Override 357 | public void onPaused(Download download) { 358 | } 359 | 360 | @Override 361 | public void onResumed(Download download) { 362 | } 363 | 364 | @Override 365 | public void onCancelled(Download download) { 366 | synchronized(sharedLock) { 367 | removeFromMaps(download.getId()); 368 | fetch.delete(download.getId()); 369 | } 370 | } 371 | 372 | @Override 373 | public void onRemoved(Download download) { 374 | } 375 | 376 | @Override 377 | public void onDeleted(Download download) { 378 | } 379 | 380 | @Override 381 | public void onAdded(Download download) { 382 | } 383 | 384 | @Override 385 | public void onQueued(Download download, boolean b) { 386 | } 387 | 388 | @Override 389 | public void onWaitingNetwork(Download download) { 390 | } 391 | 392 | @Override 393 | public void onError(Download download, Error error, Throwable throwable) { 394 | synchronized(sharedLock) { 395 | RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); 396 | 397 | if (config != null ) { 398 | WritableMap params = Arguments.createMap(); 399 | params.putString("id", config.id); 400 | 401 | int convertedErrCode = convertErrorCode(error); 402 | params.putInt("errorcode", convertedErrCode); 403 | 404 | if (error == Error.UNKNOWN && throwable != null) { 405 | params.putString("error", throwable.getLocalizedMessage()); 406 | Log.e(getName(), "UNKNOWN Error in download: " + throwable.getLocalizedMessage()); 407 | } else { 408 | params.putString("error", error.toString()); 409 | Log.e(getName(), "Error in download: " + error.toString() + ":" + error.getValue()); 410 | } 411 | ee.emit("downloadFailed", params); 412 | } 413 | 414 | removeFromMaps(download.getId()); 415 | fetch.remove(download.getId()); 416 | } 417 | } 418 | 419 | @Override 420 | public void onDownloadBlockUpdated(Download download, DownloadBlock downloadBlock, int i) { 421 | } 422 | 423 | @Override 424 | public void onStarted(Download download, List list, int i) { 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /android/src/main/java/com/eko/RNBackgroundDownloaderPackage.java: -------------------------------------------------------------------------------- 1 | 2 | package com.eko; 3 | 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.bridge.NativeModule; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.uimanager.ViewManager; 12 | import com.facebook.react.bridge.JavaScriptModule; 13 | public class RNBackgroundDownloaderPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNBackgroundDownloaderModule(reactContext)); 17 | } 18 | 19 | @Override 20 | public List createViewManagers(ReactApplicationContext reactContext) { 21 | return Collections.emptyList(); 22 | } 23 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:metro-react-native-babel-preset"] 3 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { NativeModules, NativeEventEmitter } from 'react-native'; 2 | const { RNBackgroundDownloader } = NativeModules; 3 | const RNBackgroundDownloaderEmitter = new NativeEventEmitter(RNBackgroundDownloader); 4 | import DownloadTask from './lib/downloadTask'; 5 | 6 | const tasksMap = new Map(); 7 | let headers = {}; 8 | 9 | RNBackgroundDownloaderEmitter.addListener('downloadProgress', events => { 10 | for (let event of events) { 11 | let task = tasksMap.get(event.id); 12 | if (task) { 13 | task._onProgress(event.percent, event.written, event.total); 14 | } 15 | } 16 | }); 17 | 18 | RNBackgroundDownloaderEmitter.addListener('downloadComplete', event => { 19 | let task = tasksMap.get(event.id); 20 | if (task) { 21 | task._onDone(event.location); 22 | } 23 | tasksMap.delete(event.id); 24 | }); 25 | 26 | RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => { 27 | let task = tasksMap.get(event.id); 28 | if (task) { 29 | task._onError(event.error, event.errorcode); 30 | } 31 | tasksMap.delete(event.id); 32 | }); 33 | 34 | RNBackgroundDownloaderEmitter.addListener('downloadBegin', event => { 35 | let task = tasksMap.get(event.id); 36 | if (task) { 37 | task._onBegin(event.expectedBytes); 38 | } 39 | }); 40 | 41 | export function setHeaders(h = {}) { 42 | if (typeof h !== 'object') { 43 | throw new Error('[RNBackgroundDownloader] headers must be an object'); 44 | } 45 | headers = h; 46 | } 47 | 48 | export function checkForExistingDownloads() { 49 | return RNBackgroundDownloader.checkForExistingDownloads() 50 | .then(foundTasks => { 51 | return foundTasks.map(taskInfo => { 52 | let task = new DownloadTask(taskInfo); 53 | if (taskInfo.state === RNBackgroundDownloader.TaskRunning) { 54 | task.state = 'DOWNLOADING'; 55 | } else if (taskInfo.state === RNBackgroundDownloader.TaskSuspended) { 56 | task.state = 'PAUSED'; 57 | } else if (taskInfo.state === RNBackgroundDownloader.TaskCanceling) { 58 | task.stop(); 59 | return null; 60 | } else if (taskInfo.state === RNBackgroundDownloader.TaskCompleted) { 61 | if (taskInfo.bytesWritten === taskInfo.totalBytes) { 62 | task.state = 'DONE'; 63 | } else { 64 | // IOS completed the download but it was not done. 65 | return null; 66 | } 67 | } 68 | tasksMap.set(taskInfo.id, task); 69 | return task; 70 | }).filter(task => task !== null); 71 | }); 72 | } 73 | 74 | export function download(options) { 75 | if (!options.id || !options.url || !options.destination) { 76 | throw new Error('[RNBackgroundDownloader] id, url and destination are required'); 77 | } 78 | if (options.headers && typeof options.headers === 'object') { 79 | options.headers = { 80 | ...headers, 81 | ...options.headers 82 | }; 83 | } else { 84 | options.headers = headers; 85 | } 86 | RNBackgroundDownloader.download(options); 87 | let task = new DownloadTask(options.id); 88 | tasksMap.set(options.id, task); 89 | return task; 90 | } 91 | 92 | export const directories = { 93 | documents: RNBackgroundDownloader.documents 94 | }; 95 | 96 | export const Network = { 97 | WIFI_ONLY: RNBackgroundDownloader.OnlyWifi, 98 | ALL: RNBackgroundDownloader.AllNetworks 99 | }; 100 | 101 | export const Priority = { 102 | HIGH: RNBackgroundDownloader.PriorityHigh, 103 | MEDIUM: RNBackgroundDownloader.PriorityNormal, 104 | LOW: RNBackgroundDownloader.PriorityLow 105 | }; 106 | 107 | export default { 108 | download, 109 | checkForExistingDownloads, 110 | setHeaders, 111 | directories, 112 | Network, 113 | Priority 114 | }; 115 | -------------------------------------------------------------------------------- /ios/RNBGDTaskConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // TaskConfig.h 3 | // EkoApp 4 | // 5 | // Created by Elad Gil on 21/11/2017. 6 | // Copyright © 2017 Eko. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RNBGDTaskConfig : NSObject 12 | 13 | @property NSString * _Nonnull id; 14 | @property NSString * _Nonnull destination; 15 | @property BOOL reportedBegin; 16 | 17 | - (id _Nullable )initWithDictionary: (NSDictionary *_Nonnull)dict; 18 | 19 | @end 20 | 21 | @implementation RNBGDTaskConfig 22 | 23 | - (id _Nullable )initWithDictionary: (NSDictionary *_Nonnull)dict { 24 | self = [super init]; 25 | if (self) { 26 | self.id = dict[@"id"]; 27 | self.destination = dict[@"destination"]; 28 | self.reportedBegin = NO; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { 35 | [aCoder encodeObject:self.id forKey:@"id"]; 36 | [aCoder encodeObject:self.destination forKey:@"destination"]; 37 | [aCoder encodeBool:self.reportedBegin forKey:@"reportedBegin"]; 38 | } 39 | 40 | - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder { 41 | self = [super init]; 42 | if (self) { 43 | self.id = [aDecoder decodeObjectForKey:@"id"]; 44 | self.destination = [aDecoder decodeObjectForKey:@"destination"]; 45 | self.reportedBegin = [aDecoder decodeBoolForKey:@"reportedBegin"]; 46 | } 47 | 48 | return self; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /ios/RNBackgroundDownloader.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNFileBackgroundDownload.h 3 | // EkoApp 4 | // 5 | // Created by Elad Gil on 20/11/2017. 6 | // Copyright © 2017 Eko. All rights reserved. 7 | // 8 | // 9 | #import 10 | #if __has_include() 11 | #import 12 | #import 13 | #elif __has_include("RCTBridgeModule.h") 14 | #import "RCTBridgeModule.h" 15 | #import "RCTEventEmitter.h" 16 | #endif 17 | 18 | typedef void (^CompletionHandler)(); 19 | 20 | @interface RNBackgroundDownloader : RCTEventEmitter 21 | 22 | + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /ios/RNBackgroundDownloader.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNFileBackgroundDownload.m 3 | // EkoApp 4 | // 5 | // Created by Elad Gil on 20/11/2017. 6 | // Copyright © 2017 Eko. All rights reserved. 7 | // 8 | // 9 | #import "RNBackgroundDownloader.h" 10 | #import "RNBGDTaskConfig.h" 11 | 12 | #define ID_TO_CONFIG_MAP_KEY @"com.eko.bgdownloadidmap" 13 | 14 | static CompletionHandler storedCompletionHandler; 15 | 16 | @implementation RNBackgroundDownloader { 17 | NSURLSession *urlSession; 18 | NSURLSessionConfiguration *sessionConfig; 19 | NSMutableDictionary *taskToConfigMap; 20 | NSMutableDictionary *idToTaskMap; 21 | NSMutableDictionary *idToResumeDataMap; 22 | NSMutableDictionary *idToPercentMap; 23 | NSMutableDictionary *progressReports; 24 | NSDate *lastProgressReport; 25 | NSNumber *sharedLock; 26 | } 27 | 28 | RCT_EXPORT_MODULE(); 29 | 30 | - (dispatch_queue_t)methodQueue 31 | { 32 | return dispatch_queue_create("com.eko.backgrounddownloader", DISPATCH_QUEUE_SERIAL); 33 | } 34 | 35 | + (BOOL)requiresMainQueueSetup { 36 | return YES; 37 | } 38 | 39 | - (NSArray *)supportedEvents { 40 | return @[@"downloadComplete", @"downloadProgress", @"downloadFailed", @"downloadBegin"]; 41 | } 42 | 43 | - (NSDictionary *)constantsToExport { 44 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 45 | 46 | return @{ 47 | @"documents": [paths firstObject], 48 | @"TaskRunning": @(NSURLSessionTaskStateRunning), 49 | @"TaskSuspended": @(NSURLSessionTaskStateSuspended), 50 | @"TaskCanceling": @(NSURLSessionTaskStateCanceling), 51 | @"TaskCompleted": @(NSURLSessionTaskStateCompleted) 52 | }; 53 | } 54 | 55 | - (id) init { 56 | self = [super init]; 57 | if (self) { 58 | taskToConfigMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:ID_TO_CONFIG_MAP_KEY]]; 59 | if (taskToConfigMap == nil) { 60 | taskToConfigMap = [[NSMutableDictionary alloc] init]; 61 | } 62 | idToTaskMap = [[NSMutableDictionary alloc] init]; 63 | idToResumeDataMap= [[NSMutableDictionary alloc] init]; 64 | idToPercentMap = [[NSMutableDictionary alloc] init]; 65 | NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; 66 | NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; 67 | sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessonIdentifier]; 68 | progressReports = [[NSMutableDictionary alloc] init]; 69 | lastProgressReport = [[NSDate alloc] init]; 70 | sharedLock = [NSNumber numberWithInt:1]; 71 | } 72 | return self; 73 | } 74 | 75 | - (void)lazyInitSession { 76 | if (urlSession == nil) { 77 | urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; 78 | } 79 | } 80 | 81 | - (void)removeTaskFromMap: (NSURLSessionTask *)task { 82 | @synchronized (sharedLock) { 83 | NSNumber *taskId = @(task.taskIdentifier); 84 | RNBGDTaskConfig *taskConfig = taskToConfigMap[taskId]; 85 | 86 | [taskToConfigMap removeObjectForKey:taskId]; 87 | [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY]; 88 | 89 | if (taskConfig) { 90 | [idToTaskMap removeObjectForKey:taskConfig.id]; 91 | [idToPercentMap removeObjectForKey:taskConfig.id]; 92 | } 93 | if (taskToConfigMap.count == 0) { 94 | [urlSession invalidateAndCancel]; 95 | urlSession = nil; 96 | } 97 | } 98 | } 99 | 100 | + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler { 101 | NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; 102 | NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; 103 | if ([sessonIdentifier isEqualToString:identifier]) { 104 | storedCompletionHandler = completionHandler; 105 | } 106 | } 107 | 108 | 109 | #pragma mark - JS exported methods 110 | RCT_EXPORT_METHOD(download: (NSDictionary *) options) { 111 | NSString *identifier = options[@"id"]; 112 | NSString *url = options[@"url"]; 113 | NSString *destination = options[@"destination"]; 114 | NSDictionary *headers = options[@"headers"]; 115 | if (identifier == nil || url == nil || destination == nil) { 116 | NSLog(@"[RNBackgroundDownloader] - [Error] id, url and destination must be set"); 117 | return; 118 | } 119 | [self lazyInitSession]; 120 | 121 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; 122 | if (headers != nil) { 123 | for (NSString *headerKey in headers) { 124 | [request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey]; 125 | } 126 | } 127 | 128 | @synchronized (sharedLock) { 129 | NSURLSessionDownloadTask __strong *task = [urlSession downloadTaskWithRequest:request]; 130 | RNBGDTaskConfig *taskConfig = [[RNBGDTaskConfig alloc] initWithDictionary: @{@"id": identifier, @"destination": destination}]; 131 | 132 | taskToConfigMap[@(task.taskIdentifier)] = taskConfig; 133 | [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY]; 134 | 135 | idToTaskMap[identifier] = task; 136 | idToPercentMap[identifier] = @0.0; 137 | 138 | [task resume]; 139 | } 140 | } 141 | 142 | RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) { 143 | @synchronized (sharedLock) { 144 | NSURLSessionDownloadTask *task = idToTaskMap[identifier]; 145 | if (task != nil && task.state == NSURLSessionTaskStateRunning) { 146 | [task suspend]; 147 | } 148 | } 149 | } 150 | 151 | RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { 152 | @synchronized (sharedLock) { 153 | NSURLSessionDownloadTask *task = idToTaskMap[identifier]; 154 | if (task != nil && task.state == NSURLSessionTaskStateSuspended) { 155 | [task resume]; 156 | } 157 | } 158 | } 159 | 160 | RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) { 161 | @synchronized (sharedLock) { 162 | NSURLSessionDownloadTask *task = idToTaskMap[identifier]; 163 | if (task != nil) { 164 | [task cancel]; 165 | [self removeTaskFromMap:task]; 166 | } 167 | } 168 | } 169 | 170 | RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { 171 | [self lazyInitSession]; 172 | [urlSession getTasksWithCompletionHandler:^(NSArray * _Nonnull dataTasks, NSArray * _Nonnull uploadTasks, NSArray * _Nonnull downloadTasks) { 173 | NSMutableArray *idsFound = [[NSMutableArray alloc] init]; 174 | @synchronized (sharedLock) { 175 | for (NSURLSessionDownloadTask *foundTask in downloadTasks) { 176 | NSURLSessionDownloadTask __strong *task = foundTask; 177 | RNBGDTaskConfig *taskConfig = taskToConfigMap[@(task.taskIdentifier)]; 178 | if (taskConfig) { 179 | if (task.state == NSURLSessionTaskStateCompleted && task.countOfBytesReceived < task.countOfBytesExpectedToReceive) { 180 | if (task.error && task.error.code == -999 && task.error.userInfo[NSURLSessionDownloadTaskResumeData] != nil) { 181 | task = [urlSession downloadTaskWithResumeData:task.error.userInfo[NSURLSessionDownloadTaskResumeData]]; 182 | } else { 183 | task = [urlSession downloadTaskWithURL:foundTask.currentRequest.URL]; 184 | } 185 | [task resume]; 186 | } 187 | NSNumber *percent = foundTask.countOfBytesExpectedToReceive > 0 ? [NSNumber numberWithFloat:(float)task.countOfBytesReceived/(float)foundTask.countOfBytesExpectedToReceive] : @0.0; 188 | [idsFound addObject:@{ 189 | @"id": taskConfig.id, 190 | @"state": [NSNumber numberWithInt: task.state], 191 | @"bytesWritten": [NSNumber numberWithLongLong:task.countOfBytesReceived], 192 | @"totalBytes": [NSNumber numberWithLongLong:foundTask.countOfBytesExpectedToReceive], 193 | @"percent": percent 194 | }]; 195 | taskConfig.reportedBegin = YES; 196 | taskToConfigMap[@(task.taskIdentifier)] = taskConfig; 197 | idToTaskMap[taskConfig.id] = task; 198 | idToPercentMap[taskConfig.id] = percent; 199 | } else { 200 | [task cancel]; 201 | } 202 | } 203 | resolve(idsFound); 204 | } 205 | }]; 206 | } 207 | 208 | #pragma mark - NSURLSessionDownloadDelegate methods 209 | - (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location { 210 | @synchronized (sharedLock) { 211 | RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)]; 212 | if (taskCofig != nil) { 213 | NSFileManager *fileManager = [NSFileManager defaultManager]; 214 | NSURL *destURL = [NSURL fileURLWithPath:taskCofig.destination]; 215 | [fileManager createDirectoryAtURL:[destURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; 216 | [fileManager removeItemAtURL:destURL error:nil]; 217 | NSError *moveError; 218 | BOOL moved = [fileManager moveItemAtURL:location toURL:destURL error:&moveError]; 219 | if (self.bridge) { 220 | if (moved) { 221 | [self sendEventWithName:@"downloadComplete" body:@{@"id": taskCofig.id}]; 222 | } else { 223 | [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [moveError localizedDescription]}]; 224 | } 225 | } 226 | [self removeTaskFromMap:downloadTask]; 227 | } 228 | } 229 | } 230 | 231 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { 232 | } 233 | 234 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { 235 | @synchronized (sharedLock) { 236 | RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)]; 237 | if (taskCofig != nil) { 238 | if (!taskCofig.reportedBegin) { 239 | [self sendEventWithName:@"downloadBegin" body:@{@"id": taskCofig.id, @"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite]}]; 240 | taskCofig.reportedBegin = YES; 241 | } 242 | 243 | NSNumber *prevPercent = idToPercentMap[taskCofig.id]; 244 | NSNumber *percent = [NSNumber numberWithFloat:(float)totalBytesWritten/(float)totalBytesExpectedToWrite]; 245 | if ([percent floatValue] - [prevPercent floatValue] > 0.01f) { 246 | progressReports[taskCofig.id] = @{@"id": taskCofig.id, @"written": [NSNumber numberWithLongLong: totalBytesWritten], @"total": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"percent": percent}; 247 | idToPercentMap[taskCofig.id] = percent; 248 | } 249 | 250 | NSDate *now = [[NSDate alloc] init]; 251 | if ([now timeIntervalSinceDate:lastProgressReport] > 1.5 && progressReports.count > 0) { 252 | if (self.bridge) { 253 | [self sendEventWithName:@"downloadProgress" body:[progressReports allValues]]; 254 | } 255 | lastProgressReport = now; 256 | [progressReports removeAllObjects]; 257 | } 258 | } 259 | } 260 | } 261 | 262 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { 263 | @synchronized (sharedLock) { 264 | RNBGDTaskConfig *taskCofig = taskToConfigMap[@(task.taskIdentifier)]; 265 | if (error != nil && error.code != -999 && taskCofig != nil) { 266 | if (self.bridge) { 267 | [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [error localizedDescription]}]; 268 | } 269 | [self removeTaskFromMap:task]; 270 | } 271 | } 272 | } 273 | 274 | - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { 275 | if (storedCompletionHandler) { 276 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 277 | storedCompletionHandler(); 278 | storedCompletionHandler = nil; 279 | }]; 280 | } 281 | } 282 | 283 | #pragma mark - serialization 284 | - (NSData *)serialize: (id)obj { 285 | return [NSKeyedArchiver archivedDataWithRootObject:obj]; 286 | } 287 | 288 | - (id)deserialize: (NSData *)data { 289 | if (data == nil) { 290 | return nil; 291 | } 292 | 293 | return [NSKeyedUnarchiver unarchiveObjectWithData:data]; 294 | } 295 | 296 | @end 297 | -------------------------------------------------------------------------------- /ios/RNBackgroundDownloader.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3E7B58A1CC2AC0600A0062D /* RNBackgroundDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNBackgroundDownloader.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libRNBackgroundDownloader.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNBackgroundDownloader.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 565BF70F208F2C7C00F66231 /* RNBGDTaskConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNBGDTaskConfig.h; sourceTree = ""; }; 28 | B3E7B5881CC2AC0600A0062D /* RNBackgroundDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNBackgroundDownloader.h; sourceTree = ""; }; 29 | B3E7B5891CC2AC0600A0062D /* RNBackgroundDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNBackgroundDownloader.m; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 134814211AA4EA7D00B7C361 /* Products */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | 134814201AA4EA6300B7C361 /* libRNBackgroundDownloader.a */, 47 | ); 48 | name = Products; 49 | sourceTree = ""; 50 | }; 51 | 58B511D21A9E6C8500147676 = { 52 | isa = PBXGroup; 53 | children = ( 54 | 565BF70F208F2C7C00F66231 /* RNBGDTaskConfig.h */, 55 | B3E7B5881CC2AC0600A0062D /* RNBackgroundDownloader.h */, 56 | B3E7B5891CC2AC0600A0062D /* RNBackgroundDownloader.m */, 57 | 134814211AA4EA7D00B7C361 /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | /* End PBXGroup section */ 62 | 63 | /* Begin PBXNativeTarget section */ 64 | 58B511DA1A9E6C8500147676 /* RNBackgroundDownloader */ = { 65 | isa = PBXNativeTarget; 66 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNBackgroundDownloader" */; 67 | buildPhases = ( 68 | 58B511D71A9E6C8500147676 /* Sources */, 69 | 58B511D81A9E6C8500147676 /* Frameworks */, 70 | 58B511D91A9E6C8500147676 /* CopyFiles */, 71 | ); 72 | buildRules = ( 73 | ); 74 | dependencies = ( 75 | ); 76 | name = RNBackgroundDownloader; 77 | productName = RCTDataManager; 78 | productReference = 134814201AA4EA6300B7C361 /* libRNBackgroundDownloader.a */; 79 | productType = "com.apple.product-type.library.static"; 80 | }; 81 | /* End PBXNativeTarget section */ 82 | 83 | /* Begin PBXProject section */ 84 | 58B511D31A9E6C8500147676 /* Project object */ = { 85 | isa = PBXProject; 86 | attributes = { 87 | LastUpgradeCheck = 0610; 88 | ORGANIZATIONNAME = Facebook; 89 | TargetAttributes = { 90 | 58B511DA1A9E6C8500147676 = { 91 | CreatedOnToolsVersion = 6.1.1; 92 | }; 93 | }; 94 | }; 95 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNBackgroundDownloader" */; 96 | compatibilityVersion = "Xcode 3.2"; 97 | developmentRegion = English; 98 | hasScannedForEncodings = 0; 99 | knownRegions = ( 100 | en, 101 | ); 102 | mainGroup = 58B511D21A9E6C8500147676; 103 | productRefGroup = 58B511D21A9E6C8500147676; 104 | projectDirPath = ""; 105 | projectRoot = ""; 106 | targets = ( 107 | 58B511DA1A9E6C8500147676 /* RNBackgroundDownloader */, 108 | ); 109 | }; 110 | /* End PBXProject section */ 111 | 112 | /* Begin PBXSourcesBuildPhase section */ 113 | 58B511D71A9E6C8500147676 /* Sources */ = { 114 | isa = PBXSourcesBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | B3E7B58A1CC2AC0600A0062D /* RNBackgroundDownloader.m in Sources */, 118 | ); 119 | runOnlyForDeploymentPostprocessing = 0; 120 | }; 121 | /* End PBXSourcesBuildPhase section */ 122 | 123 | /* Begin XCBuildConfiguration section */ 124 | 58B511ED1A9E6C8500147676 /* Debug */ = { 125 | isa = XCBuildConfiguration; 126 | buildSettings = { 127 | ALWAYS_SEARCH_USER_PATHS = NO; 128 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 129 | CLANG_CXX_LIBRARY = "libc++"; 130 | CLANG_ENABLE_MODULES = YES; 131 | CLANG_ENABLE_OBJC_ARC = YES; 132 | CLANG_WARN_BOOL_CONVERSION = YES; 133 | CLANG_WARN_CONSTANT_CONVERSION = YES; 134 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 135 | CLANG_WARN_EMPTY_BODY = YES; 136 | CLANG_WARN_ENUM_CONVERSION = YES; 137 | CLANG_WARN_INT_CONVERSION = YES; 138 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 139 | CLANG_WARN_UNREACHABLE_CODE = YES; 140 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 141 | COPY_PHASE_STRIP = NO; 142 | ENABLE_STRICT_OBJC_MSGSEND = YES; 143 | GCC_C_LANGUAGE_STANDARD = gnu99; 144 | GCC_DYNAMIC_NO_PIC = NO; 145 | GCC_OPTIMIZATION_LEVEL = 0; 146 | GCC_PREPROCESSOR_DEFINITIONS = ( 147 | "DEBUG=1", 148 | "$(inherited)", 149 | ); 150 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 151 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 152 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 153 | GCC_WARN_UNDECLARED_SELECTOR = YES; 154 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 155 | GCC_WARN_UNUSED_FUNCTION = YES; 156 | GCC_WARN_UNUSED_VARIABLE = YES; 157 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 158 | MTL_ENABLE_DEBUG_INFO = YES; 159 | ONLY_ACTIVE_ARCH = YES; 160 | SDKROOT = iphoneos; 161 | }; 162 | name = Debug; 163 | }; 164 | 58B511EE1A9E6C8500147676 /* Release */ = { 165 | isa = XCBuildConfiguration; 166 | buildSettings = { 167 | ALWAYS_SEARCH_USER_PATHS = NO; 168 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 169 | CLANG_CXX_LIBRARY = "libc++"; 170 | CLANG_ENABLE_MODULES = YES; 171 | CLANG_ENABLE_OBJC_ARC = YES; 172 | CLANG_WARN_BOOL_CONVERSION = YES; 173 | CLANG_WARN_CONSTANT_CONVERSION = YES; 174 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 175 | CLANG_WARN_EMPTY_BODY = YES; 176 | CLANG_WARN_ENUM_CONVERSION = YES; 177 | CLANG_WARN_INT_CONVERSION = YES; 178 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 179 | CLANG_WARN_UNREACHABLE_CODE = YES; 180 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 181 | COPY_PHASE_STRIP = YES; 182 | ENABLE_NS_ASSERTIONS = NO; 183 | ENABLE_STRICT_OBJC_MSGSEND = YES; 184 | GCC_C_LANGUAGE_STANDARD = gnu99; 185 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 186 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 187 | GCC_WARN_UNDECLARED_SELECTOR = YES; 188 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 189 | GCC_WARN_UNUSED_FUNCTION = YES; 190 | GCC_WARN_UNUSED_VARIABLE = YES; 191 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 192 | MTL_ENABLE_DEBUG_INFO = NO; 193 | SDKROOT = iphoneos; 194 | VALIDATE_PRODUCT = YES; 195 | }; 196 | name = Release; 197 | }; 198 | 58B511F01A9E6C8500147676 /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | HEADER_SEARCH_PATHS = ( 202 | "$(inherited)", 203 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 204 | "$(SRCROOT)/../../../React/**", 205 | "$(SRCROOT)/../../react-native/React/**", 206 | ); 207 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 208 | OTHER_LDFLAGS = "-ObjC"; 209 | PRODUCT_NAME = RNBackgroundDownloader; 210 | SKIP_INSTALL = YES; 211 | }; 212 | name = Debug; 213 | }; 214 | 58B511F11A9E6C8500147676 /* Release */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | HEADER_SEARCH_PATHS = ( 218 | "$(inherited)", 219 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 220 | "$(SRCROOT)/../../../React/**", 221 | "$(SRCROOT)/../../react-native/React/**", 222 | ); 223 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 224 | OTHER_LDFLAGS = "-ObjC"; 225 | PRODUCT_NAME = RNBackgroundDownloader; 226 | SKIP_INSTALL = YES; 227 | }; 228 | name = Release; 229 | }; 230 | /* End XCBuildConfiguration section */ 231 | 232 | /* Begin XCConfigurationList section */ 233 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNBackgroundDownloader" */ = { 234 | isa = XCConfigurationList; 235 | buildConfigurations = ( 236 | 58B511ED1A9E6C8500147676 /* Debug */, 237 | 58B511EE1A9E6C8500147676 /* Release */, 238 | ); 239 | defaultConfigurationIsVisible = 0; 240 | defaultConfigurationName = Release; 241 | }; 242 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNBackgroundDownloader" */ = { 243 | isa = XCConfigurationList; 244 | buildConfigurations = ( 245 | 58B511F01A9E6C8500147676 /* Debug */, 246 | 58B511F11A9E6C8500147676 /* Release */, 247 | ); 248 | defaultConfigurationIsVisible = 0; 249 | defaultConfigurationName = Release; 250 | }; 251 | /* End XCConfigurationList section */ 252 | }; 253 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 254 | } 255 | -------------------------------------------------------------------------------- /lib/downloadTask.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | const { RNBackgroundDownloader } = NativeModules; 3 | 4 | function validateHandler(handler) { 5 | if (!(typeof handler === 'function')) { 6 | throw new TypeError(`[RNBackgroundDownloader] expected argument to be a function, got: ${typeof handler}`); 7 | } 8 | } 9 | export default class DownloadTask { 10 | state = 'PENDING' 11 | percent = 0 12 | bytesWritten = 0 13 | totalBytes = 0 14 | 15 | constructor(taskInfo) { 16 | if (typeof taskInfo === 'string') { 17 | this.id = taskInfo; 18 | } else { 19 | this.id = taskInfo.id; 20 | this.percent = taskInfo.percent; 21 | this.bytesWritten = taskInfo.bytesWritten; 22 | this.totalBytes = taskInfo.totalBytes; 23 | } 24 | } 25 | 26 | begin(handler) { 27 | validateHandler(handler); 28 | this._beginHandler = handler; 29 | return this; 30 | } 31 | 32 | progress(handler) { 33 | validateHandler(handler); 34 | this._progressHandler = handler; 35 | return this; 36 | } 37 | 38 | done(handler) { 39 | validateHandler(handler); 40 | this._doneHandler = handler; 41 | return this; 42 | } 43 | 44 | error(handler) { 45 | validateHandler(handler); 46 | this._errorHandler = handler; 47 | return this; 48 | } 49 | 50 | _onBegin(expectedBytes) { 51 | this.state = 'DOWNLOADING'; 52 | if (this._beginHandler) { 53 | this._beginHandler(expectedBytes); 54 | } 55 | } 56 | 57 | _onProgress(percent, bytesWritten, totalBytes) { 58 | this.percent = percent; 59 | this.bytesWritten = bytesWritten; 60 | this.totalBytes = totalBytes; 61 | if (this._progressHandler) { 62 | this._progressHandler(percent, bytesWritten, totalBytes); 63 | } 64 | } 65 | 66 | _onDone() { 67 | this.state = 'DONE'; 68 | if (this._doneHandler) { 69 | this._doneHandler(); 70 | } 71 | } 72 | 73 | _onError(error, errorCode) { 74 | this.state = 'FAILED'; 75 | if (this._errorHandler) { 76 | this._errorHandler(error, errorCode); 77 | } 78 | } 79 | 80 | pause() { 81 | this.state = 'PAUSED'; 82 | RNBackgroundDownloader.pauseTask(this.id); 83 | } 84 | 85 | resume() { 86 | this.state = 'DOWNLOADING'; 87 | RNBackgroundDownloader.resumeTask(this.id); 88 | } 89 | 90 | stop() { 91 | this.state = 'STOPPED'; 92 | RNBackgroundDownloader.stopTask(this.id); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-background-downloader", 3 | "version": "2.3.4", 4 | "description": "A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prepublish": "jest && npm run lint", 9 | "lint": "eslint index.js lib/**" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/EkoLabs/react-native-background-downloader.git" 14 | }, 15 | "keywords": [ 16 | "react-native", 17 | "background", 18 | "download", 19 | "large files" 20 | ], 21 | "files": [ 22 | "README.md", 23 | "LICENSE", 24 | "react-native-background-downloader.podspec", 25 | "package.json", 26 | "index.js", 27 | "lib/", 28 | "ios/", 29 | "android/build.gradle", 30 | "android/src/" 31 | ], 32 | "author": { 33 | "name": "Eko labs", 34 | "url": "https://developer.helloeko.com", 35 | "email": "dev@helloeko.com" 36 | }, 37 | "contributors": [ 38 | { 39 | "name": "Elad Gil" 40 | } 41 | ], 42 | "license": "Apache-2.0", 43 | "peerDependencies": { 44 | "react-native": ">=0.57.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.4.0", 48 | "@babel/runtime": "^7.4.2", 49 | "@react-native-community/eslint-config": "^0.0.3", 50 | "babel-jest": "^24.5.0", 51 | "eslint": "^5.16.0", 52 | "immer": "^3.2.0", 53 | "jest": "^24.5.0", 54 | "metro-react-native-babel-preset": "^0.53.1", 55 | "react": "16.8.3", 56 | "react-native": "0.59.9", 57 | "react-native-fs": "^2.14.1", 58 | "react-native-vector-icons": "^6.6.0", 59 | "react-test-renderer": "16.8.3" 60 | }, 61 | "jest": { 62 | "preset": "react-native", 63 | "setupFiles": [ 64 | "./__mocks__/RNBackgroundDownloader.js", 65 | "./node_modules/react-native/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /react-native-background-downloader.podspec: -------------------------------------------------------------------------------- 1 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 2 | 3 | Pod::Spec.new do |s| 4 | s.name = package['name'] 5 | s.version = package['version'] 6 | s.summary = 'React Native background downloader' 7 | s.description = package['description'] 8 | s.author = package['author'] 9 | s.homepage = package['repository']['url'] 10 | s.license = package['license'] 11 | s.platform = :ios, '7.0' 12 | s.source = { git: 'https://github.com/EkoLabs/react-native-background-downloader.git', tag: 'master' } 13 | s.source_files = 'ios/**/*.{h,m}' 14 | s.requires_arc = true 15 | 16 | s.dependency 'React' 17 | end 18 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependency: { 3 | platforms: { 4 | ios: { 5 | project: "./ios/RNBackgroundDownloader.xcodeproj" 6 | }, 7 | android: { 8 | sourceDir: "./android" 9 | } 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /testApp/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /testApp/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | * @flow 7 | */ 8 | 9 | import React, { Component } from 'react'; 10 | import { Text, SafeAreaView, TextInput, Button, FlatList, View, AsyncStorage, TouchableOpacity, Slider } from 'react-native'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import RNFS from 'react-native-fs'; 13 | import produce from 'immer'; 14 | import RNBGD from '../index'; 15 | import styles from './Style'; 16 | 17 | const testURL = 'https://speed.hetzner.de/100MB.bin'; 18 | const urlRegex = /^(?:https?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/; 19 | 20 | function isValid(url) { 21 | return urlRegex.test(url); 22 | } 23 | 24 | export default class App extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.idsToData = {}; 28 | } 29 | 30 | state = { 31 | url: '', 32 | status: 'idle', 33 | percent: 0, 34 | downloads: [], 35 | downloadsData: {}, 36 | }; 37 | 38 | async componentDidMount() { 39 | const tasks = await RNBGD.checkForExistingDownloads(); 40 | if (tasks && tasks.length) { 41 | await this.loadDownloads(); 42 | const downloadsData = {}; 43 | const downloads = []; 44 | for (let task of tasks) { 45 | downloads.push(task.id); 46 | downloadsData[task.id] = { 47 | url: this.idsToData[task.id].url, 48 | percent: task.percent, 49 | total: task.totalBytes, 50 | status: task.state === 'DOWNLOADING' ? 'downloading' : 'paused', 51 | task: task 52 | }; 53 | this.attachToTask(task, this.idsToData[task.id].filePath); 54 | } 55 | this.setState({ 56 | downloadsData, 57 | downloads 58 | }); 59 | } 60 | } 61 | 62 | saveDownloads() { 63 | AsyncStorage.setItem('idsToData', JSON.stringify(this.idsToData)); 64 | } 65 | 66 | async loadDownloads() { 67 | const mapStr = await AsyncStorage.getItem('idsToData'); 68 | try { 69 | this.idsToData = JSON.parse(mapStr) || {}; 70 | } catch (e) { 71 | console.error(e); 72 | } 73 | } 74 | 75 | pauseOrResume(id) { 76 | let newStatus; 77 | const download = this.state.downloadsData[id]; 78 | if (download.status === 'downloading') { 79 | download.task.pause(); 80 | newStatus = 'paused'; 81 | } else if (download.status === 'paused') { 82 | download.task.resume(); 83 | newStatus = 'downloading'; 84 | } else { 85 | console.error(`Unknown status for play or pause: ${download.status}`); 86 | return; 87 | } 88 | 89 | this.setState(produce(draft => { 90 | draft.downloadsData[id].status = newStatus; 91 | })); 92 | } 93 | 94 | cancel(id) { 95 | const download = this.state.downloadsData[id]; 96 | download.task.stop(); 97 | delete this.idsToData[id]; 98 | this.saveDownloads(); 99 | this.setState(produce(draft => { 100 | delete draft.downloadsData[id]; 101 | draft.downloads.splice(draft.downloads.indexOf(id), 1); 102 | })); 103 | } 104 | 105 | renderRow({ item: downloadId }) { 106 | const download = this.state.downloadsData[downloadId]; 107 | let iconName = 'ios-pause'; 108 | if (download.status === 'paused') { 109 | iconName = 'ios-play'; 110 | } 111 | 112 | return ( 113 | 114 | 115 | 116 | {downloadId} 117 | {download.url} 118 | 119 | 123 | 124 | 125 | this.pauseOrResume(downloadId)}> 126 | 127 | 128 | this.cancel(downloadId)}> 129 | 130 | 131 | 132 | 133 | ); 134 | } 135 | 136 | attachToTask(task, filePath) { 137 | task.begin(expectedBytes => { 138 | this.setState(produce(draft => { 139 | draft.downloadsData[task.id].total = expectedBytes; 140 | draft.downloadsData[task.id].status = 'downloading'; 141 | })); 142 | }) 143 | .progress(percent => { 144 | this.setState(produce(draft => { 145 | draft.downloadsData[task.id].percent = percent; 146 | })); 147 | }) 148 | .done(async() => { 149 | try { 150 | console.log(`Finished downloading: ${task.id}, deleting it...`); 151 | await RNFS.unlink(filePath); 152 | console.log(`Deleted ${task.id}`); 153 | } catch (e) { 154 | console.error(e); 155 | } 156 | delete this.idsToData[task.id]; 157 | this.saveDownloads(); 158 | this.setState(produce(draft => { 159 | delete draft.downloadsData[task.id]; 160 | draft.downloads.splice(draft.downloads.indexOf(task.id), 1); 161 | })); 162 | }) 163 | .error(err => { 164 | console.error(`Download ${task.id} has an error: ${err}`); 165 | delete this.idsToData[task.id]; 166 | this.saveDownloads(); 167 | this.setState(produce(draft => { 168 | delete draft.downloadsData[task.id]; 169 | draft.downloads.splice(draft.downloads.indexOf(task.id), 1); 170 | })); 171 | }); 172 | } 173 | 174 | addDownload() { 175 | const id = Math.random() 176 | .toString(36) 177 | .substr(2, 6); 178 | const filePath = `${RNBGD.directories.documents}/${id}`; 179 | const url = this.state.url || testURL; 180 | const task = RNBGD.download({ 181 | id: id, 182 | url: url, 183 | destination: filePath, 184 | }); 185 | this.attachToTask(task, filePath); 186 | this.idsToData[id] = { 187 | url, 188 | filePath 189 | }; 190 | this.saveDownloads(); 191 | 192 | this.setState(produce(draft => { 193 | draft.downloadsData[id] = { 194 | url: url, 195 | status: 'idle', 196 | task: task 197 | }; 198 | draft.downloads.push(id); 199 | draft.url = ''; 200 | })); 201 | } 202 | 203 | render() { 204 | return ( 205 | 206 | { 214 | this.setState({ url: text.toLowerCase() }); 215 | }} 216 | value={this.state.url} 217 | /> 218 |