@capgo/capacitor-downloader 8.4.1 → 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.
@@ -0,0 +1,420 @@
1
+ package ee.forgr.capacitor.plugin.downloader;
2
+
3
+ import android.content.Context;
4
+ import android.net.ConnectivityManager;
5
+ import android.net.Network;
6
+ import android.net.NetworkCapabilities;
7
+ import com.getcapacitor.JSObject;
8
+ import java.io.File;
9
+ import java.io.FileOutputStream;
10
+ import java.io.IOException;
11
+ import java.io.InputStream;
12
+ import java.util.Iterator;
13
+ import java.util.Map;
14
+ import java.util.concurrent.ConcurrentHashMap;
15
+ import java.util.concurrent.Semaphore;
16
+ import java.util.concurrent.atomic.AtomicBoolean;
17
+ import okhttp3.Call;
18
+ import okhttp3.OkHttpClient;
19
+ import okhttp3.Request;
20
+ import okhttp3.Response;
21
+ import okhttp3.ResponseBody;
22
+
23
+ public class OkHttpDownloadExecutor {
24
+
25
+ public static final long TRACKED_DOWNLOAD_ID = -2L;
26
+ private static final int MAX_CONCURRENT_TRANSFERS = 4;
27
+
28
+ public enum State {
29
+ PENDING,
30
+ RUNNING,
31
+ DONE,
32
+ ERROR,
33
+ CANCELLED
34
+ }
35
+
36
+ public static final class ActiveDownload {
37
+
38
+ public final String id;
39
+ public volatile State state = State.PENDING;
40
+ public volatile float progress;
41
+ public volatile long bytesWritten;
42
+ public volatile long bytesTotal;
43
+ public volatile String errorMessage;
44
+ public volatile Call call;
45
+ public volatile File tempFile;
46
+ public volatile File committedDestination;
47
+ public volatile Network boundWifiNetwork;
48
+ public final AtomicBoolean cancelled = new AtomicBoolean(false);
49
+
50
+ ActiveDownload(String id) {
51
+ this.id = id;
52
+ }
53
+ }
54
+
55
+ private final OkHttpClient defaultClient;
56
+ private final Map<String, ActiveDownload> activeDownloads = new ConcurrentHashMap<>();
57
+ private final Semaphore transferSlots = new Semaphore(MAX_CONCURRENT_TRANSFERS);
58
+ private final Context context;
59
+
60
+ public OkHttpDownloadExecutor(Context context) {
61
+ this.context = context.getApplicationContext();
62
+ this.defaultClient = new OkHttpClient.Builder().followRedirects(true).followSslRedirects(false).build();
63
+ }
64
+
65
+ public ActiveDownload getActive(String id) {
66
+ return activeDownloads.get(id);
67
+ }
68
+
69
+ /**
70
+ * Reserve an in-memory slot before the transfer id is published to JS. Prevents a race where
71
+ * {@code stop()} runs between marker publication and worker registration.
72
+ */
73
+ public boolean beginTransfer(String id) {
74
+ ActiveDownload active = new ActiveDownload(id);
75
+ active.state = State.PENDING;
76
+ return activeDownloads.putIfAbsent(id, active) == null;
77
+ }
78
+
79
+ public void abandonTransfer(String id) {
80
+ ActiveDownload active = activeDownloads.remove(id);
81
+ if (active != null && active.call != null) {
82
+ active.call.cancel();
83
+ }
84
+ }
85
+
86
+ public void start(
87
+ String id,
88
+ String url,
89
+ File destinationFile,
90
+ JSObject headers,
91
+ String network,
92
+ ProgressReporter reporter,
93
+ CompletionListener completionListener
94
+ ) {
95
+ ActiveDownload active = activeDownloads.get(id);
96
+ if (active == null) {
97
+ throw new IllegalStateException("Download not registered");
98
+ }
99
+
100
+ Network wifiNetwork = resolveWifiNetworkForTransfer(network);
101
+ if ("wifi-only".equals(network) && (wifiNetwork == null || !isWifiNetworkUsable(wifiNetwork))) {
102
+ activeDownloads.remove(id, active);
103
+ throw new IllegalStateException("Wi-Fi required for this download");
104
+ }
105
+
106
+ boolean acquired = false;
107
+ try {
108
+ transferSlots.acquire();
109
+ acquired = true;
110
+
111
+ synchronized (active) {
112
+ if (active.cancelled.get()) {
113
+ finishCancelled(active, null, id, completionListener);
114
+ return;
115
+ }
116
+ }
117
+
118
+ if (wifiNetwork != null) {
119
+ active.boundWifiNetwork = wifiNetwork;
120
+ }
121
+ OkHttpClient client = buildClientForNetwork(network, wifiNetwork);
122
+
123
+ File tempFile = null;
124
+ try {
125
+ synchronized (active) {
126
+ if (active.cancelled.get()) {
127
+ finishCancelled(active, null, id, completionListener);
128
+ return;
129
+ }
130
+ }
131
+
132
+ Request.Builder requestBuilder = new Request.Builder().url(url);
133
+ if (headers != null) {
134
+ Iterator<String> it = headers.keys();
135
+ if (it != null) {
136
+ while (it.hasNext()) {
137
+ String key = it.next();
138
+ requestBuilder.addHeader(key, headers.getString(key));
139
+ }
140
+ }
141
+ }
142
+
143
+ Request request = requestBuilder.build();
144
+ Call call = client.newCall(request);
145
+ synchronized (active) {
146
+ if (active.cancelled.get()) {
147
+ call.cancel();
148
+ finishCancelled(active, null, id, completionListener);
149
+ return;
150
+ }
151
+ active.call = call;
152
+ }
153
+ active.state = State.RUNNING;
154
+
155
+ File parent = destinationFile.getParentFile();
156
+ if (parent != null && !parent.exists() && !parent.mkdirs()) {
157
+ throw new IOException("Could not create destination directory");
158
+ }
159
+
160
+ tempFile = createTempDownloadFile(destinationFile, parent);
161
+ active.tempFile = tempFile;
162
+
163
+ try (Response response = call.execute()) {
164
+ if (active.cancelled.get()) {
165
+ finishCancelled(active, tempFile, id, completionListener);
166
+ return;
167
+ }
168
+
169
+ if (url.regionMatches(true, 0, "https:", 0, 6) && !response.request().url().isHttps()) {
170
+ active.state = State.ERROR;
171
+ active.errorMessage = "HTTPS to HTTP redirect is not allowed";
172
+ tempFile.delete();
173
+ completionListener.onFinished(id, false, false, active.errorMessage);
174
+ return;
175
+ }
176
+
177
+ int code = response.code();
178
+ if (code == 206) {
179
+ active.state = State.ERROR;
180
+ active.errorMessage = "Partial content (HTTP 206) is not supported";
181
+ tempFile.delete();
182
+ completionListener.onFinished(id, false, false, active.errorMessage);
183
+ return;
184
+ }
185
+
186
+ if (!response.isSuccessful()) {
187
+ active.state = State.ERROR;
188
+ active.errorMessage = "HTTP " + code;
189
+ tempFile.delete();
190
+ completionListener.onFinished(id, false, false, active.errorMessage);
191
+ return;
192
+ }
193
+
194
+ ResponseBody body = response.body();
195
+ if (body == null) {
196
+ active.state = State.ERROR;
197
+ active.errorMessage = "Empty response body";
198
+ tempFile.delete();
199
+ completionListener.onFinished(id, false, false, active.errorMessage);
200
+ return;
201
+ }
202
+
203
+ long total = body.contentLength();
204
+ active.bytesTotal = total > 0 ? total : 0L;
205
+
206
+ try (InputStream input = body.byteStream(); FileOutputStream output = new FileOutputStream(tempFile)) {
207
+ byte[] buffer = new byte[8192];
208
+ long written = 0L;
209
+ int read;
210
+ int lastReportedPercent = -1;
211
+ while ((read = input.read(buffer)) != -1) {
212
+ if (active.cancelled.get()) {
213
+ finishCancelled(active, tempFile, id, completionListener);
214
+ return;
215
+ }
216
+ output.write(buffer, 0, read);
217
+ written += read;
218
+ active.bytesWritten = written;
219
+ if (total > 0) {
220
+ active.progress = (float) written / (float) total;
221
+ }
222
+ int percent = (int) (active.progress * 100f);
223
+ if (percent != lastReportedPercent) {
224
+ lastReportedPercent = percent;
225
+ reporter.reportProgress(id, active.progress);
226
+ }
227
+ }
228
+ }
229
+
230
+ if (!commitTempToDestination(active, tempFile, destinationFile)) {
231
+ finishCancelled(active, tempFile, id, completionListener);
232
+ return;
233
+ }
234
+ tempFile = null;
235
+
236
+ reporter.reportProgress(id, 1f);
237
+ completionListener.onFinished(id, true, false, null);
238
+ }
239
+ } catch (IOException e) {
240
+ if (tempFile != null && tempFile.exists()) {
241
+ tempFile.delete();
242
+ }
243
+ if (active.cancelled.get()) {
244
+ active.state = State.CANCELLED;
245
+ completionListener.onFinished(id, false, true, null);
246
+ } else {
247
+ active.state = State.ERROR;
248
+ active.errorMessage = e.getMessage() != null ? e.getMessage() : "Download failed";
249
+ completionListener.onFinished(id, false, false, active.errorMessage);
250
+ }
251
+ } catch (RuntimeException e) {
252
+ activeDownloads.remove(id, active);
253
+ throw e;
254
+ }
255
+ } catch (InterruptedException e) {
256
+ Thread.currentThread().interrupt();
257
+ active.state = State.ERROR;
258
+ active.errorMessage = "Download interrupted";
259
+ completionListener.onFinished(id, false, false, active.errorMessage);
260
+ } finally {
261
+ if (acquired) {
262
+ transferSlots.release();
263
+ }
264
+ }
265
+ }
266
+
267
+ private boolean commitTempToDestination(ActiveDownload active, File tempFile, File destinationFile) throws IOException {
268
+ synchronized (active) {
269
+ if (active.cancelled.get()) {
270
+ tempFile.delete();
271
+ active.state = State.CANCELLED;
272
+ return false;
273
+ }
274
+ active.tempFile = null;
275
+ if (destinationFile.exists() && !destinationFile.delete()) {
276
+ throw new IOException("Could not replace existing destination file");
277
+ }
278
+ if (active.cancelled.get()) {
279
+ tempFile.delete();
280
+ active.state = State.CANCELLED;
281
+ return false;
282
+ }
283
+ if (!tempFile.renameTo(destinationFile)) {
284
+ throw new IOException("Could not move downloaded file into place");
285
+ }
286
+ if (active.cancelled.get()) {
287
+ destinationFile.delete();
288
+ active.state = State.CANCELLED;
289
+ return false;
290
+ }
291
+ active.committedDestination = destinationFile;
292
+ active.bytesWritten = destinationFile.length();
293
+ active.progress = 1f;
294
+ active.state = State.DONE;
295
+ return true;
296
+ }
297
+ }
298
+
299
+ private void finishCancelled(ActiveDownload active, File tempFile, String id, CompletionListener completionListener) {
300
+ active.state = State.CANCELLED;
301
+ File toDelete = tempFile;
302
+ if (toDelete == null) {
303
+ synchronized (active) {
304
+ toDelete = active.tempFile;
305
+ }
306
+ }
307
+ if (toDelete != null && toDelete.exists()) {
308
+ toDelete.delete();
309
+ }
310
+ completionListener.onFinished(id, false, true, null);
311
+ }
312
+
313
+ private static File createTempDownloadFile(File destinationFile, File parent) throws IOException {
314
+ String baseName = destinationFile.getName();
315
+ if (baseName.isEmpty()) {
316
+ baseName = "download";
317
+ }
318
+ String prefix = baseName.length() >= 3 ? baseName : "dl" + baseName;
319
+ if (prefix.length() < 3) {
320
+ prefix = "cap";
321
+ }
322
+ return File.createTempFile(prefix + "-", ".capgo-download", parent != null ? parent : destinationFile.getParentFile());
323
+ }
324
+
325
+ private OkHttpClient buildClientForNetwork(String network, Network wifiNetwork) {
326
+ if (!"wifi-only".equals(network)) {
327
+ return defaultClient;
328
+ }
329
+ if (wifiNetwork == null) {
330
+ throw new IllegalStateException("Wi-Fi required for this download");
331
+ }
332
+ return defaultClient.newBuilder().socketFactory(wifiNetwork.getSocketFactory()).build();
333
+ }
334
+
335
+ Network resolveWifiNetworkForTransfer(String network) {
336
+ if (!"wifi-only".equals(network)) {
337
+ return null;
338
+ }
339
+ return findWifiNetwork();
340
+ }
341
+
342
+ private Network findWifiNetwork() {
343
+ ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
344
+ if (connectivityManager == null) {
345
+ return null;
346
+ }
347
+ for (Network network : connectivityManager.getAllNetworks()) {
348
+ NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
349
+ if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
350
+ return network;
351
+ }
352
+ }
353
+ return null;
354
+ }
355
+
356
+ public boolean cancel(String id, File destinationFile) {
357
+ ActiveDownload active = activeDownloads.get(id);
358
+ if (active == null) {
359
+ if (destinationFile != null && destinationFile.exists()) {
360
+ return destinationFile.delete();
361
+ }
362
+ return false;
363
+ }
364
+ active.cancelled.set(true);
365
+ Call call = active.call;
366
+ if (call != null) {
367
+ call.cancel();
368
+ }
369
+ synchronized (active) {
370
+ File temp = active.tempFile;
371
+ if (temp != null && temp.exists()) {
372
+ temp.delete();
373
+ }
374
+ File committed = active.committedDestination;
375
+ if (committed != null && committed.exists()) {
376
+ committed.delete();
377
+ } else if (destinationFile != null && destinationFile.exists() && active.state == State.DONE) {
378
+ destinationFile.delete();
379
+ }
380
+ }
381
+ activeDownloads.remove(id, active);
382
+ return true;
383
+ }
384
+
385
+ public void clearCompleted(String id) {
386
+ ActiveDownload active = activeDownloads.get(id);
387
+ if (active != null && (active.state == State.DONE || active.state == State.ERROR || active.state == State.CANCELLED)) {
388
+ activeDownloads.remove(id, active);
389
+ }
390
+ }
391
+
392
+ public boolean isWifiSatisfied(String network) {
393
+ if (!"wifi-only".equals(network)) {
394
+ return true;
395
+ }
396
+ Network wifi = resolveWifiNetworkForTransfer(network);
397
+ return wifi != null && isWifiNetworkUsable(wifi);
398
+ }
399
+
400
+ private boolean isWifiNetworkUsable(Network network) {
401
+ ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
402
+ if (connectivityManager == null || network == null) {
403
+ return false;
404
+ }
405
+ NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
406
+ return (
407
+ capabilities != null &&
408
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
409
+ capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
410
+ );
411
+ }
412
+
413
+ public interface ProgressReporter {
414
+ void reportProgress(String id, float progress);
415
+ }
416
+
417
+ public interface CompletionListener {
418
+ void onFinished(String id, boolean success, boolean cancelled, String errorMessage);
419
+ }
420
+ }
package/dist/docs.json CHANGED
@@ -27,7 +27,7 @@
27
27
  },
28
28
  {
29
29
  "name": "example",
30
- "text": "```typescript\nconst task = await Downloader.download({\n id: 'my-download',\n url: 'https://example.com/file.pdf',\n destination: 'downloads/file.pdf',\n notification: 'hidden', // Android: no system notification; use downloadProgress events\n downloadCompletedNotificationText: 'Your file is ready', // opt-in completion alert\n});\n```"
30
+ "text": "```typescript\nconst task = await Downloader.download({\n id: 'my-download',\n url: 'https://example.com/file.pdf',\n destination: 'files/offline/package.zip',\n androidHttpEngine: 'auto',\n notification: 'hidden', // Android OkHttp: no notification during download; use downloadProgress events\n downloadCompletedNotificationText: 'Your file is ready', // opt-in completion alert\n});\n```"
31
31
  }
32
32
  ],
33
33
  "docs": "Start a new download task.",
@@ -364,7 +364,7 @@
364
364
  {
365
365
  "name": "destination",
366
366
  "tags": [],
367
- "docs": "Local file path where the download will be saved",
367
+ "docs": "Local file path where the download will be saved.\n\nOn Android, relative paths without `androidHttpEngine` behave like earlier releases: every relative path,\nincluding `cache/...` and `files/...`, is stored under app external files via DownloadManager.\n\nWhen you set `androidHttpEngine` to `'auto'` or `'okhttp'`, optional roots apply:\n- `cache/...` saves under app cache (OkHttp with `'auto'`).\n- `files/...` saves under app-private internal storage (OkHttp with `'auto'`).\n- `public-downloads/...` saves to the public Downloads folder (DownloadManager).\n- Any other relative path saves under app external files (DownloadManager with `'auto'`).\nAbsolute paths and `file://` URIs are supported; `'auto'` picks OkHttp when the path is under cache, data, or files dirs.",
368
368
  "complexTypes": [],
369
369
  "type": "string"
370
370
  },
@@ -397,15 +397,27 @@
397
397
  "name": "since"
398
398
  }
399
399
  ],
400
- "docs": "Android DownloadManager notification visibility.\n\n- `'completed'` (default): show while downloading and keep the notification after completion.\n- `'progress'`: show while downloading, remove the notification when finished.\n- `'hidden'`: no system notification; use plugin progress events instead.\n\nIgnored on iOS and Web.",
400
+ "docs": "Android DownloadManager notification visibility.\n\n- `'completed'` (default): show while downloading and keep the notification after completion.\n- `'progress'`: show while downloading, remove the notification when finished.\n- `'hidden'`: no DownloadManager progress notification; use plugin progress events instead.\n `downloadCompletedNotificationText` can still post a completion notification when the transfer succeeds.\n\nOn the OkHttp Android path, notifications are not shown during the transfer. With `'completed'`, a single\ncompletion notification may be posted when the file finishes (Android 13+ requires runtime\n`POST_NOTIFICATIONS` in the host app). Ignored on iOS and Web.",
401
401
  "complexTypes": [],
402
402
  "type": "'completed' | 'progress' | 'hidden' | undefined"
403
403
  },
404
+ {
405
+ "name": "androidHttpEngine",
406
+ "tags": [
407
+ {
408
+ "text": "next release",
409
+ "name": "since"
410
+ }
411
+ ],
412
+ "docs": "Android HTTP stack used for the download.\n\n- Omitted: always DownloadManager with legacy destination resolution (same as releases before this option).\n- `'auto'`: OkHttp for app-private destinations (`cache/...`, `files/...`, or an absolute path under\n cache/data/files when extended roots apply); DownloadManager for legacy relative paths under app external storage\n and `public-downloads/...`.\n- `'okhttp'`: always use OkHttp for extended-root destinations. Rejects `public-downloads/...` paths. Transfers run\n in the app process and do not continue after the process is killed (unlike DownloadManager on those paths).\n- `'downloadManager'`: always use the system DownloadManager with legacy destination resolution.\n\nIgnored on iOS and Web.",
413
+ "complexTypes": [],
414
+ "type": "'auto' | 'okhttp' | 'downloadManager' | undefined"
415
+ },
404
416
  {
405
417
  "name": "downloadCompletedNotificationText",
406
418
  "tags": [
407
419
  {
408
- "text": "8.4.0",
420
+ "text": "next release",
409
421
  "name": "since"
410
422
  }
411
423
  ],
@@ -18,7 +18,19 @@ export interface DownloadOptions {
18
18
  id: string;
19
19
  /** URL of the file to download */
20
20
  url: string;
21
- /** Local file path where the download will be saved */
21
+ /**
22
+ * Local file path where the download will be saved.
23
+ *
24
+ * On Android, relative paths without `androidHttpEngine` behave like earlier releases: every relative path,
25
+ * including `cache/...` and `files/...`, is stored under app external files via DownloadManager.
26
+ *
27
+ * When you set `androidHttpEngine` to `'auto'` or `'okhttp'`, optional roots apply:
28
+ * - `cache/...` saves under app cache (OkHttp with `'auto'`).
29
+ * - `files/...` saves under app-private internal storage (OkHttp with `'auto'`).
30
+ * - `public-downloads/...` saves to the public Downloads folder (DownloadManager).
31
+ * - Any other relative path saves under app external files (DownloadManager with `'auto'`).
32
+ * Absolute paths and `file://` URIs are supported; `'auto'` picks OkHttp when the path is under cache, data, or files dirs.
33
+ */
22
34
  destination: string;
23
35
  /** Optional HTTP headers to include in the request */
24
36
  headers?: {
@@ -33,13 +45,32 @@ export interface DownloadOptions {
33
45
  *
34
46
  * - `'completed'` (default): show while downloading and keep the notification after completion.
35
47
  * - `'progress'`: show while downloading, remove the notification when finished.
36
- * - `'hidden'`: no system notification; use plugin progress events instead.
48
+ * - `'hidden'`: no DownloadManager progress notification; use plugin progress events instead.
49
+ * `downloadCompletedNotificationText` can still post a completion notification when the transfer succeeds.
37
50
  *
38
- * Ignored on iOS and Web.
51
+ * On the OkHttp Android path, notifications are not shown during the transfer. With `'completed'`, a single
52
+ * completion notification may be posted when the file finishes (Android 13+ requires runtime
53
+ * `POST_NOTIFICATIONS` in the host app). Ignored on iOS and Web.
39
54
  *
40
55
  * @since 8.2.0
41
56
  */
42
57
  notification?: 'completed' | 'progress' | 'hidden';
58
+ /**
59
+ * Android HTTP stack used for the download.
60
+ *
61
+ * - Omitted: always DownloadManager with legacy destination resolution (same as releases before this option).
62
+ * - `'auto'`: OkHttp for app-private destinations (`cache/...`, `files/...`, or an absolute path under
63
+ * cache/data/files when extended roots apply); DownloadManager for legacy relative paths under app external storage
64
+ * and `public-downloads/...`.
65
+ * - `'okhttp'`: always use OkHttp for extended-root destinations. Rejects `public-downloads/...` paths. Transfers run
66
+ * in the app process and do not continue after the process is killed (unlike DownloadManager on those paths).
67
+ * - `'downloadManager'`: always use the system DownloadManager with legacy destination resolution.
68
+ *
69
+ * Ignored on iOS and Web.
70
+ *
71
+ * @since next release
72
+ */
73
+ androidHttpEngine?: 'auto' | 'okhttp' | 'downloadManager';
43
74
  /**
44
75
  * Optional localized text for a system notification when the download finishes.
45
76
  *
@@ -50,7 +81,7 @@ export interface DownloadOptions {
50
81
  * Omitted or empty means no completion notification from the plugin (use
51
82
  * `downloadCompleted` events while the app is open).
52
83
  *
53
- * @since 8.4.0
84
+ * @since next release
54
85
  */
55
86
  downloadCompletedNotificationText?: string;
56
87
  }
@@ -69,8 +100,9 @@ export interface CapacitorDownloaderPlugin {
69
100
  * const task = await Downloader.download({
70
101
  * id: 'my-download',
71
102
  * url: 'https://example.com/file.pdf',
72
- * destination: 'downloads/file.pdf',
73
- * notification: 'hidden', // Android: no system notification; use downloadProgress events
103
+ * destination: 'files/offline/package.zip',
104
+ * androidHttpEngine: 'auto',
105
+ * notification: 'hidden', // Android OkHttp: no notification during download; use downloadProgress events
74
106
  * downloadCompletedNotificationText: 'Your file is ready', // opt-in completion alert
75
107
  * });
76
108
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Represents the current state and progress of a download task.\n */\nexport interface DownloadTask {\n /** Unique identifier for the download task */\n id: string;\n /** Download progress from 0 to 100 */\n progress: number;\n /** Current state of the download */\n state: 'PENDING' | 'RUNNING' | 'PAUSED' | 'DONE' | 'ERROR';\n}\n\n/**\n * Configuration options for starting a download.\n */\nexport interface DownloadOptions {\n /** Unique identifier for this download task */\n id: string;\n /** URL of the file to download */\n url: string;\n /** Local file path where the download will be saved */\n destination: string;\n /** Optional HTTP headers to include in the request */\n headers?: { [key: string]: string };\n /** Network type requirement for download */\n network?: 'cellular' | 'wifi-only';\n /** Download priority level */\n priority?: 'high' | 'normal' | 'low';\n /**\n * Android DownloadManager notification visibility.\n *\n * - `'completed'` (default): show while downloading and keep the notification after completion.\n * - `'progress'`: show while downloading, remove the notification when finished.\n * - `'hidden'`: no system notification; use plugin progress events instead.\n *\n * Ignored on iOS and Web.\n *\n * @since 8.2.0\n */\n notification?: 'completed' | 'progress' | 'hidden';\n /**\n * Optional localized text for a system notification when the download finishes.\n *\n * When set, iOS and Android may show a local notification after a successful download\n * if the user granted notification permission. If permission is denied, the download\n * still completes and no notification is shown. Tapping the notification reopens the app.\n *\n * Omitted or empty means no completion notification from the plugin (use\n * `downloadCompleted` events while the app is open).\n *\n * @since 8.4.0\n */\n downloadCompletedNotificationText?: string;\n}\n\n/**\n * Capacitor plugin for downloading files with background support.\n * Provides resumable downloads with progress tracking.\n */\nexport interface CapacitorDownloaderPlugin {\n /**\n * Start a new download task.\n *\n * @param options - Download configuration\n * @returns Promise with initial download task status\n * @example\n * ```typescript\n * const task = await Downloader.download({\n * id: 'my-download',\n * url: 'https://example.com/file.pdf',\n * destination: 'downloads/file.pdf',\n * notification: 'hidden', // Android: no system notification; use downloadProgress events\n * downloadCompletedNotificationText: 'Your file is ready', // opt-in completion alert\n * });\n * ```\n */\n download(options: DownloadOptions): Promise<DownloadTask>;\n\n /**\n * Pause an active download.\n * Download can be resumed later from the same position.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when paused\n */\n pause(options: { id: string }): Promise<void>;\n\n /**\n * Resume a paused download.\n * Continues from where it was paused.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when resumed\n */\n resume(options: { id: string }): Promise<void>;\n\n /**\n * Stop and cancel a download permanently.\n * Downloaded data will be deleted.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when stopped\n */\n stop(options: { id: string }): Promise<void>;\n\n /**\n * Check the current status of a download.\n *\n * @param options - Options containing the download task ID\n * @returns Promise with current download task status\n */\n checkStatus(options: { id: string }): Promise<DownloadTask>;\n\n /**\n * Get information about a downloaded file.\n *\n * @param options - Options containing the file path\n * @returns Promise with file size and MIME type\n */\n getFileInfo(options: { path: string }): Promise<{ size: number; type: string }>;\n\n /**\n * Listen for download progress updates.\n * Fired periodically as download progresses.\n *\n * @param eventName - Must be 'downloadProgress'\n * @param listenerFunc - Callback receiving progress updates\n * @returns Promise with listener handle for removal\n * @example\n * ```typescript\n * const listener = await Downloader.addListener('downloadProgress', (data) => {\n * console.log(`Download ${data.id}: ${data.progress}%`);\n * });\n * ```\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (progress: {\n id: string;\n progress: number;\n /** Bytes written so far */\n bytesWritten?: number;\n /** Total bytes expected, or 0 when the server did not report a length */\n bytesTotal?: number;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download completion.\n * Fired when a download finishes successfully.\n *\n * @param eventName - Must be 'downloadCompleted'\n * @param listenerFunc - Callback receiving completion notification\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadCompleted',\n listenerFunc: (result: { id: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download failures.\n * Fired when a download encounters an error.\n *\n * @param eventName - Must be 'downloadFailed'\n * @param listenerFunc - Callback receiving error information\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadFailed',\n listenerFunc: (error: { id: string; error: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Remove all event listeners.\n * Cleanup method to prevent memory leaks.\n *\n * @returns Promise that resolves when all listeners removed\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the plugin version number.\n *\n * @returns Promise with version string\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Represents the current state and progress of a download task.\n */\nexport interface DownloadTask {\n /** Unique identifier for the download task */\n id: string;\n /** Download progress from 0 to 100 */\n progress: number;\n /** Current state of the download */\n state: 'PENDING' | 'RUNNING' | 'PAUSED' | 'DONE' | 'ERROR';\n}\n\n/**\n * Configuration options for starting a download.\n */\nexport interface DownloadOptions {\n /** Unique identifier for this download task */\n id: string;\n /** URL of the file to download */\n url: string;\n /**\n * Local file path where the download will be saved.\n *\n * On Android, relative paths without `androidHttpEngine` behave like earlier releases: every relative path,\n * including `cache/...` and `files/...`, is stored under app external files via DownloadManager.\n *\n * When you set `androidHttpEngine` to `'auto'` or `'okhttp'`, optional roots apply:\n * - `cache/...` saves under app cache (OkHttp with `'auto'`).\n * - `files/...` saves under app-private internal storage (OkHttp with `'auto'`).\n * - `public-downloads/...` saves to the public Downloads folder (DownloadManager).\n * - Any other relative path saves under app external files (DownloadManager with `'auto'`).\n * Absolute paths and `file://` URIs are supported; `'auto'` picks OkHttp when the path is under cache, data, or files dirs.\n */\n destination: string;\n /** Optional HTTP headers to include in the request */\n headers?: { [key: string]: string };\n /** Network type requirement for download */\n network?: 'cellular' | 'wifi-only';\n /** Download priority level */\n priority?: 'high' | 'normal' | 'low';\n /**\n * Android DownloadManager notification visibility.\n *\n * - `'completed'` (default): show while downloading and keep the notification after completion.\n * - `'progress'`: show while downloading, remove the notification when finished.\n * - `'hidden'`: no DownloadManager progress notification; use plugin progress events instead.\n * `downloadCompletedNotificationText` can still post a completion notification when the transfer succeeds.\n *\n * On the OkHttp Android path, notifications are not shown during the transfer. With `'completed'`, a single\n * completion notification may be posted when the file finishes (Android 13+ requires runtime\n * `POST_NOTIFICATIONS` in the host app). Ignored on iOS and Web.\n *\n * @since 8.2.0\n */\n notification?: 'completed' | 'progress' | 'hidden';\n /**\n * Android HTTP stack used for the download.\n *\n * - Omitted: always DownloadManager with legacy destination resolution (same as releases before this option).\n * - `'auto'`: OkHttp for app-private destinations (`cache/...`, `files/...`, or an absolute path under\n * cache/data/files when extended roots apply); DownloadManager for legacy relative paths under app external storage\n * and `public-downloads/...`.\n * - `'okhttp'`: always use OkHttp for extended-root destinations. Rejects `public-downloads/...` paths. Transfers run\n * in the app process and do not continue after the process is killed (unlike DownloadManager on those paths).\n * - `'downloadManager'`: always use the system DownloadManager with legacy destination resolution.\n *\n * Ignored on iOS and Web.\n *\n * @since next release\n */\n androidHttpEngine?: 'auto' | 'okhttp' | 'downloadManager';\n /**\n * Optional localized text for a system notification when the download finishes.\n *\n * When set, iOS and Android may show a local notification after a successful download\n * if the user granted notification permission. If permission is denied, the download\n * still completes and no notification is shown. Tapping the notification reopens the app.\n *\n * Omitted or empty means no completion notification from the plugin (use\n * `downloadCompleted` events while the app is open).\n *\n * @since next release\n */\n downloadCompletedNotificationText?: string;\n}\n\n/**\n * Capacitor plugin for downloading files with background support.\n * Provides resumable downloads with progress tracking.\n */\nexport interface CapacitorDownloaderPlugin {\n /**\n * Start a new download task.\n *\n * @param options - Download configuration\n * @returns Promise with initial download task status\n * @example\n * ```typescript\n * const task = await Downloader.download({\n * id: 'my-download',\n * url: 'https://example.com/file.pdf',\n * destination: 'files/offline/package.zip',\n * androidHttpEngine: 'auto',\n * notification: 'hidden', // Android OkHttp: no notification during download; use downloadProgress events\n * downloadCompletedNotificationText: 'Your file is ready', // opt-in completion alert\n * });\n * ```\n */\n download(options: DownloadOptions): Promise<DownloadTask>;\n\n /**\n * Pause an active download.\n * Download can be resumed later from the same position.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when paused\n */\n pause(options: { id: string }): Promise<void>;\n\n /**\n * Resume a paused download.\n * Continues from where it was paused.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when resumed\n */\n resume(options: { id: string }): Promise<void>;\n\n /**\n * Stop and cancel a download permanently.\n * Downloaded data will be deleted.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when stopped\n */\n stop(options: { id: string }): Promise<void>;\n\n /**\n * Check the current status of a download.\n *\n * @param options - Options containing the download task ID\n * @returns Promise with current download task status\n */\n checkStatus(options: { id: string }): Promise<DownloadTask>;\n\n /**\n * Get information about a downloaded file.\n *\n * @param options - Options containing the file path\n * @returns Promise with file size and MIME type\n */\n getFileInfo(options: { path: string }): Promise<{ size: number; type: string }>;\n\n /**\n * Listen for download progress updates.\n * Fired periodically as download progresses.\n *\n * @param eventName - Must be 'downloadProgress'\n * @param listenerFunc - Callback receiving progress updates\n * @returns Promise with listener handle for removal\n * @example\n * ```typescript\n * const listener = await Downloader.addListener('downloadProgress', (data) => {\n * console.log(`Download ${data.id}: ${data.progress}%`);\n * });\n * ```\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (progress: {\n id: string;\n progress: number;\n /** Bytes written so far */\n bytesWritten?: number;\n /** Total bytes expected, or 0 when the server did not report a length */\n bytesTotal?: number;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download completion.\n * Fired when a download finishes successfully.\n *\n * @param eventName - Must be 'downloadCompleted'\n * @param listenerFunc - Callback receiving completion notification\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadCompleted',\n listenerFunc: (result: { id: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download failures.\n * Fired when a download encounters an error.\n *\n * @param eventName - Must be 'downloadFailed'\n * @param listenerFunc - Callback receiving error information\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadFailed',\n listenerFunc: (error: { id: string; error: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Remove all event listeners.\n * Cleanup method to prevent memory leaks.\n *\n * @returns Promise that resolves when all listeners removed\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the plugin version number.\n *\n * @returns Promise with version string\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
@@ -8,7 +8,7 @@ import Capacitor
8
8
 
9
9
  @objc(CapacitorDownloaderPlugin)
10
10
  public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
11
- private let pluginVersion: String = "8.4.1"
11
+ private let pluginVersion: String = "8.5.0"
12
12
  public let identifier = "CapacitorDownloaderPlugin"
13
13
  public let jsName = "CapacitorDownloader"
14
14
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-downloader",
3
- "version": "8.4.1",
3
+ "version": "8.5.0",
4
4
  "description": "Download file in background or foreground",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",