@capgo/capacitor-downloader 8.3.21 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/README.md +66 -10
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidDownloadDestination.java +143 -0
  5. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/AndroidHttpEngineSelector.java +33 -0
  6. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java +357 -27
  7. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationPresenter.java +91 -0
  8. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationStore.java +138 -0
  9. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletedNotificationText.java +14 -0
  10. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadCompletionNotifier.java +70 -0
  11. package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/OkHttpDownloadExecutor.java +420 -0
  12. package/dist/docs.json +27 -3
  13. package/dist/esm/definitions.d.ts +51 -5
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/ios/Sources/CapacitorDownloaderPlugin/CapacitorDownloaderPlugin.swift +86 -6
  16. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationPresenter.swift +38 -0
  17. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationStore.swift +74 -0
  18. package/ios/Sources/CapacitorDownloaderPlugin/DownloadCompletedNotificationText.swift +11 -0
  19. package/ios/Sources/CapacitorDownloaderPlugin/DownloadIdGenerationStore.swift +39 -0
  20. package/ios/Sources/CapacitorDownloaderPlugin/DownloadTaskGenerationStore.swift +47 -0
  21. package/ios/Tests/CapacitorDownloaderPluginTests/CapacitorDownloaderPluginTests.swift +121 -0
  22. package/package.json +1 -1
package/README.md CHANGED
@@ -34,7 +34,61 @@ We build, improve and fix our plugins with heavy use of AI, so issues usually ge
34
34
  - **Control**: `pause()`, `resume()` and `stop()` for each download ID.
35
35
  - **Progress events**: `downloadProgress`, `downloadCompleted` and `downloadFailed` listeners.
36
36
  - **Status and files**: `checkStatus()` for a task and `getFileInfo()` for a downloaded file.
37
- - **Platforms**: iOS and Android. iOS uses a background `URLSession`, Android uses `DownloadManager`. Not available on web.
37
+ - **Platforms**: iOS and Android. iOS uses a background `URLSession`. Android uses `DownloadManager` by default (same as earlier releases). **OkHttp** is opt in via `androidHttpEngine` for app-private destinations. Not available on web.
38
+ - **Android private storage**: set `androidHttpEngine: 'auto'` or `'okhttp'` and use `cache/...` or `files/...` for internal storage. `notification: 'hidden'` hides DownloadManager progress UI; `downloadCompletedNotificationText` can still alert on success.
39
+
40
+ ## Android OkHttp engine (next release)
41
+
42
+ **Opt in:** set `androidHttpEngine` to `'auto'` or `'okhttp'` for downloads into app-private storage (offline packs, encrypted assets, cache). When the option is omitted, every destination uses `DownloadManager` and legacy app external file paths, including existing `cache/...` and `files/...` strings.
43
+
44
+ ```typescript
45
+ await CapacitorDownloader.download({
46
+ id: 'offline-pack',
47
+ url: 'https://cdn.example.com/pack.zip',
48
+ destination: 'files/offline/pack.zip',
49
+ androidHttpEngine: 'auto', // OkHttp for files/... and cache/...
50
+ notification: 'hidden', // no system notification during transfer; listen to downloadProgress
51
+ });
52
+ ```
53
+
54
+ | `androidHttpEngine` | Behavior |
55
+ | ------------------- | -------- |
56
+ | Omitted | DownloadManager only, legacy destination paths (matches pre-option releases) |
57
+ | `'auto'` | OkHttp for `cache/...`, `files/...`, and paths under internal cache/data/files; DownloadManager for external app storage and `public-downloads/...` |
58
+ | `'okhttp'` | Always OkHttp; rejects `public-downloads/...` paths |
59
+ | `'downloadManager'` | Always system DownloadManager with legacy destination paths |
60
+
61
+ **Android `destination` prefixes**
62
+
63
+ | Prefix | Storage when `androidHttpEngine` is `'auto'` or `'okhttp'` | Storage when option is omitted |
64
+ | ------ | ----------------------------------------------------------- | ------------------------------ |
65
+ | `cache/...` | App cache dir (OkHttp with `'auto'`) | App external files (DownloadManager) |
66
+ | `files/...` | App-private files dir (OkHttp with `'auto'`) | App external files (DownloadManager) |
67
+ | `public-downloads/...` | Public Downloads folder (DownloadManager) | App external files under `public-downloads/...` (DownloadManager) |
68
+ | Other relative paths | App external files dir (DownloadManager) | App external files dir (DownloadManager) |
69
+
70
+ **Notifications on OkHttp:** no notification during the transfer. Use `notification: 'hidden'` with `downloadProgress` events, or `'completed'` to post one completion-style notification when the file finishes (not on failure). DownloadManager paths still honor `completed`, `progress`, and `hidden` as before. `downloadCompletedNotificationText` is separate and can still post on success when set.
71
+
72
+ Full guide: https://capgo.app/docs/plugins/downloader/getting-started/#android-okhttp-engine
73
+
74
+ ## Download completed notifications
75
+
76
+ Long downloads often finish while your user is in another app. Set `downloadCompletedNotificationText` on `download()` to show an opt-in, caller-localized system notification when the file is ready. Tapping it returns to your app.
77
+
78
+ - **Opt in**: omit the option (or pass empty text) to rely on `downloadCompleted` events only.
79
+ - **Permissions**: request notification permission in your app. If the user denies it, downloads still complete and the plugin skips the alert.
80
+ - **Android**: the plugin manifest already merges `POST_NOTIFICATIONS`; on Android 13+ your app only needs to request it at runtime. Works alongside the DownloadManager `notification` visibility option. While your app is in the foreground, Android still posts the completion alert when the download succeeds.
81
+ - **iOS**: request authorization with `UNUserNotificationCenter` before the download finishes. While your app is in the foreground, iOS shows a delivered notification only if your app's `UNUserNotificationCenter` delegate presents it, so without a notifications plugin it is usually silent; use `downloadCompleted` events in that case.
82
+
83
+ ```typescript
84
+ await Downloader.download({
85
+ id: 'episode-12',
86
+ url: audioUrl,
87
+ destination: 'podcasts/episode-12.mp3',
88
+ notification: 'hidden',
89
+ downloadCompletedNotificationText: t('downloads.episodeReady'),
90
+ });
91
+ ```
38
92
 
39
93
  ## Why Capacitor Downloader?
40
94
 
@@ -311,15 +365,17 @@ Represents the current state and progress of a download task.
311
365
 
312
366
  Configuration options for starting a download.
313
367
 
314
- | Prop | Type | Description | Since |
315
- | ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
316
- | **`id`** | <code>string</code> | Unique identifier for this download task | |
317
- | **`url`** | <code>string</code> | URL of the file to download | |
318
- | **`destination`** | <code>string</code> | Local file path where the download will be saved | |
319
- | **`headers`** | <code>{ [key: string]: string; }</code> | Optional HTTP headers to include in the request | |
320
- | **`network`** | <code>'cellular' \| 'wifi-only'</code> | Network type requirement for download | |
321
- | **`priority`** | <code>'high' \| 'normal' \| 'low'</code> | Download priority level | |
322
- | **`notification`** | <code>'completed' \| 'progress' \| 'hidden'</code> | Android DownloadManager notification visibility. - `'completed'` (default): show while downloading and keep the notification after completion. - `'progress'`: show while downloading, remove the notification when finished. - `'hidden'`: no system notification; use plugin progress events instead. Ignored on iOS and Web. | 8.2.0 |
368
+ | Prop | Type | Description | Since |
369
+ | --------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
370
+ | **`id`** | <code>string</code> | Unique identifier for this download task | |
371
+ | **`url`** | <code>string</code> | URL of the file to download | |
372
+ | **`destination`** | <code>string</code> | Local file path where the download will be saved. On Android, relative paths without `androidHttpEngine` behave like earlier releases: every relative path, including `cache/...` and `files/...`, is stored under app external files via DownloadManager. When you set `androidHttpEngine` to `'auto'` or `'okhttp'`, optional roots apply: - `cache/...` saves under app cache (OkHttp with `'auto'`). - `files/...` saves under app-private internal storage (OkHttp with `'auto'`). - `public-downloads/...` saves to the public Downloads folder (DownloadManager). - Any other relative path saves under app external files (DownloadManager with `'auto'`). Absolute paths and `file://` URIs are supported; `'auto'` picks OkHttp when the path is under cache, data, or files dirs. | |
373
+ | **`headers`** | <code>{ [key: string]: string; }</code> | Optional HTTP headers to include in the request | |
374
+ | **`network`** | <code>'cellular' \| 'wifi-only'</code> | Network type requirement for download | |
375
+ | **`priority`** | <code>'high' \| 'normal' \| 'low'</code> | Download priority level | |
376
+ | **`notification`** | <code>'completed' \| 'progress' \| 'hidden'</code> | Android DownloadManager notification visibility. - `'completed'` (default): show while downloading and keep the notification after completion. - `'progress'`: show while downloading, remove the notification when finished. - `'hidden'`: no DownloadManager progress notification; use plugin progress events instead. `downloadCompletedNotificationText` can still post a completion notification when the transfer succeeds. On the OkHttp Android path, notifications are not shown during the transfer. With `'completed'`, a single completion notification may be posted when the file finishes (Android 13+ requires runtime `POST_NOTIFICATIONS` in the host app). Ignored on iOS and Web. | 8.2.0 |
377
+ | **`androidHttpEngine`** | <code>'auto' \| 'okhttp' \| 'downloadManager'</code> | Android HTTP stack used for the download. - Omitted: always DownloadManager with legacy destination resolution (same as releases before this option). - `'auto'`: OkHttp for app-private destinations (`cache/...`, `files/...`, or an absolute path under cache/data/files when extended roots apply); DownloadManager for legacy relative paths under app external storage and `public-downloads/...`. - `'okhttp'`: always use OkHttp for extended-root destinations. Rejects `public-downloads/...` paths. Transfers run in the app process and do not continue after the process is killed (unlike DownloadManager on those paths). - `'downloadManager'`: always use the system DownloadManager with legacy destination resolution. Ignored on iOS and Web. | next release |
378
+ | **`downloadCompletedNotificationText`** | <code>string</code> | Optional localized text for a system notification when the download finishes. When set, iOS and Android may show a local notification after a successful download if the user granted notification permission. If permission is denied, the download still completes and no notification is shown. Tapping the notification reopens the app. Omitted or empty means no completion notification from the plugin (use `downloadCompleted` events while the app is open). | next release |
323
379
 
324
380
 
325
381
  #### PluginListenerHandle
@@ -57,8 +57,11 @@ dependencies {
57
57
  implementation project(':capacitor-android')
58
58
  annotationProcessor project(':capacitor-android')
59
59
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
60
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
60
61
  testImplementation "junit:junit:$junitVersion"
61
62
  testImplementation "org.mockito:mockito-core:5.24.0"
63
+ testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0"
64
+ testImplementation "com.squareup.okhttp3:okhttp-tls:4.12.0"
62
65
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
63
66
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
64
67
  }
@@ -1,3 +1,4 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
3
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
3
4
  </manifest>
@@ -0,0 +1,143 @@
1
+ package ee.forgr.capacitor.plugin.downloader;
2
+
3
+ import android.content.Context;
4
+ import java.io.File;
5
+ import java.io.IOException;
6
+
7
+ /**
8
+ * Resolves download destinations under app storage roots.
9
+ *
10
+ * <p>With legacy resolution (default when {@code androidHttpEngine} is omitted), relative paths match
11
+ * released plugin behavior: all paths are under app external files, including {@code cache/...} and
12
+ * {@code files/...} as literal subpaths. {@code file://} and absolute paths use the same {@code File}
13
+ * parent resolution as main: {@code new File(getExternalFilesDir(null), child)}.
14
+ *
15
+ * <p>With extended resolution ({@code androidHttpEngine: 'auto'} or {@code 'okhttp'}), relative prefixes:
16
+ * <ul>
17
+ * <li>{@code cache/...} -> {@link Context#getCacheDir()}</li>
18
+ * <li>{@code files/...} -> {@link Context#getFilesDir()} (app-private data)</li>
19
+ * <li>{@code public-downloads/...} -> public Downloads directory (DownloadManager only)</li>
20
+ * <li>other relative paths -> app external files</li>
21
+ * </ul>
22
+ */
23
+ public final class AndroidDownloadDestination {
24
+
25
+ private AndroidDownloadDestination() {}
26
+
27
+ public static ResolvedDestination resolve(Context context, String destination, boolean extendedRoots) {
28
+ if (destination == null || destination.trim().isEmpty()) {
29
+ throw new IllegalArgumentException("Missing destination");
30
+ }
31
+
32
+ String trimmed = destination.trim();
33
+
34
+ if (!extendedRoots) {
35
+ return resolveLegacyMain(context, trimmed);
36
+ }
37
+
38
+ if (trimmed.startsWith("file://")) {
39
+ File file = new File(android.net.Uri.parse(trimmed).getPath());
40
+ return new ResolvedDestination(file, preferOkHttpInAutoMode(context, file));
41
+ }
42
+
43
+ if (trimmed.startsWith("/")) {
44
+ File file = new File(trimmed);
45
+ return new ResolvedDestination(file, preferOkHttpInAutoMode(context, file));
46
+ }
47
+
48
+ if (trimmed.startsWith("cache/")) {
49
+ File base = context.getCacheDir();
50
+ File file = childUnder(base, trimmed.substring("cache/".length()));
51
+ return new ResolvedDestination(file, true);
52
+ }
53
+
54
+ if (trimmed.startsWith("files/")) {
55
+ File base = context.getFilesDir();
56
+ File file = childUnder(base, trimmed.substring("files/".length()));
57
+ return new ResolvedDestination(file, true);
58
+ }
59
+
60
+ if (trimmed.startsWith("public-downloads/")) {
61
+ File base = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS);
62
+ File file = childUnder(base, trimmed.substring("public-downloads/".length()));
63
+ return new ResolvedDestination(file, false);
64
+ }
65
+
66
+ File base = context.getExternalFilesDir(null);
67
+ boolean usedInternalFallback = base == null;
68
+ if (usedInternalFallback) {
69
+ base = context.getFilesDir();
70
+ }
71
+ File file = childUnder(base, trimmed);
72
+ boolean preferOkHttp = usedInternalFallback || preferOkHttpInAutoMode(context, file);
73
+ return new ResolvedDestination(file, preferOkHttp);
74
+ }
75
+
76
+ private static ResolvedDestination resolveLegacyMain(Context context, String trimmed) {
77
+ File base = context.getExternalFilesDir(null);
78
+ boolean usedInternalFallback = base == null;
79
+ if (usedInternalFallback) {
80
+ base = context.getFilesDir();
81
+ }
82
+ File file = new File(base, trimmed);
83
+ boolean preferOkHttp = usedInternalFallback;
84
+ return new ResolvedDestination(file, preferOkHttp);
85
+ }
86
+
87
+ static File childUnder(File base, String relative) {
88
+ if (base == null) {
89
+ throw new IllegalArgumentException("Storage root is unavailable");
90
+ }
91
+ File file = new File(base, relative);
92
+ try {
93
+ String canonical = file.getCanonicalPath();
94
+ String baseCanonical = base.getCanonicalPath();
95
+ if (!canonical.startsWith(baseCanonical + File.separator)) {
96
+ throw new IllegalArgumentException("Destination must name a file under app storage: " + relative);
97
+ }
98
+ } catch (IOException e) {
99
+ throw new IllegalArgumentException("Invalid destination path", e);
100
+ }
101
+ return file;
102
+ }
103
+
104
+ /**
105
+ * Whether {@link AndroidHttpEngineSelector} should pick OkHttp when {@code androidHttpEngine} is auto.
106
+ */
107
+ public static boolean preferOkHttpInAutoMode(Context context, File file) {
108
+ try {
109
+ String canonical = file.getCanonicalPath();
110
+ return (
111
+ isUnder(canonical, context.getCacheDir()) ||
112
+ isUnder(canonical, context.getFilesDir()) ||
113
+ isUnder(canonical, context.getDataDir())
114
+ );
115
+ } catch (IOException e) {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ private static boolean isUnder(String path, File root) {
121
+ if (root == null) {
122
+ return false;
123
+ }
124
+ try {
125
+ String rootCanonical = root.getCanonicalPath();
126
+ return path.equals(rootCanonical) || path.startsWith(rootCanonical + File.separator);
127
+ } catch (IOException e) {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ public static final class ResolvedDestination {
133
+
134
+ public final File file;
135
+ /** When true, {@code androidHttpEngine: 'auto'} selects OkHttp. */
136
+ public final boolean preferOkHttpInAutoMode;
137
+
138
+ ResolvedDestination(File file, boolean preferOkHttpInAutoMode) {
139
+ this.file = file;
140
+ this.preferOkHttpInAutoMode = preferOkHttpInAutoMode;
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,33 @@
1
+ package ee.forgr.capacitor.plugin.downloader;
2
+
3
+ /**
4
+ * Chooses between DownloadManager and OkHttp for Android downloads.
5
+ */
6
+ public final class AndroidHttpEngineSelector {
7
+
8
+ public enum Engine {
9
+ DOWNLOAD_MANAGER,
10
+ OKHTTP
11
+ }
12
+
13
+ private AndroidHttpEngineSelector() {}
14
+
15
+ /**
16
+ * @param option explicit value from {@code androidHttpEngine}: auto, okhttp, or downloadManager (null => DownloadManager, same as main)
17
+ */
18
+ public static Engine resolve(String option, boolean preferOkHttpInAutoMode) {
19
+ if (option == null || option.isEmpty()) {
20
+ return Engine.DOWNLOAD_MANAGER;
21
+ }
22
+ if ("auto".equalsIgnoreCase(option)) {
23
+ return preferOkHttpInAutoMode ? Engine.OKHTTP : Engine.DOWNLOAD_MANAGER;
24
+ }
25
+ if ("okhttp".equalsIgnoreCase(option) || "native".equalsIgnoreCase(option)) {
26
+ return Engine.OKHTTP;
27
+ }
28
+ if ("downloadManager".equalsIgnoreCase(option) || "download_manager".equalsIgnoreCase(option)) {
29
+ return Engine.DOWNLOAD_MANAGER;
30
+ }
31
+ throw new IllegalArgumentException("Invalid androidHttpEngine: " + option);
32
+ }
33
+ }