diff --git a/package.json b/package.json
index ace65222..afc165b5 100644
--- a/package.json
+++ b/package.json
@@ -70,8 +70,8 @@
],
"license": "Apache-2.0",
"engines": {
- "node": "14.x",
- "npm": "6.x",
+ "node": ">=14.x",
+ "npm": ">=6.x",
"cordova": ">=8.0.0"
},
"standard": {
diff --git a/plugin.xml b/plugin.xml
index a2b5418b..ce78f338 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -13,9 +13,9 @@
-
+
-
+
diff --git a/src/android/FileTransferBackground.java b/src/android/FileTransferBackground.java
index bdd82da4..830e63be 100644
--- a/src/android/FileTransferBackground.java
+++ b/src/android/FileTransferBackground.java
@@ -17,6 +17,7 @@
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
+import androidx.work.WorkQuery;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
@@ -46,8 +47,6 @@ public class FileTransferBackground extends CordovaPlugin {
private Data httpClientBaseConfig = Data.EMPTY;
- public static boolean workerIsStarted;
-
private ScheduledExecutorService executorService = null;
private int ccUpload;
@@ -179,19 +178,57 @@ private void initManager(String options, final CallbackContext callbackContext)
final AckDatabase ackDatabase = AckDatabase.getInstance(cordova.getContext());
- // Resend pending ACK at startup (and warmup database)
- final List uploadEvents = ackDatabase
- .uploadEventDao()
- .getAll();
-
- int ackDelay = 0;
- for (UploadEvent ack : uploadEvents) {
- executorService.schedule(() -> {
- handleAck(ack.getOutputData());
- }, ackDelay, TimeUnit.MILLISECONDS);
- ackDelay += 200;
+ // Delete any worker
+ WorkManager.getInstance(cordova.getContext()).cancelAllWork();
+ WorkManager.getInstance(cordova.getContext()).pruneWork();
+
+ ackDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ ackDatabase.pendingUploadDao().resetUploadingAsPending();
+ }
+ });
+
+ // Start workers if there is pending uploads and no worker is already running
+ try {
+ List workInfoStates = new ArrayList<>();
+ workInfoStates.add(WorkInfo.State.BLOCKED);
+ workInfoStates.add(WorkInfo.State.CANCELLED);
+ workInfoStates.add(WorkInfo.State.ENQUEUED);
+ workInfoStates.add(WorkInfo.State.RUNNING);
+ List workers = WorkManager.getInstance(cordova.getContext()).getWorkInfos(WorkQuery.Builder.fromStates(workInfoStates).build()).get();
+
+ ackDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ if (ackDatabase.pendingUploadDao().getPendingUploadsCount() > 0 && workers.size() == 0) {
+ startWorkers();
+ }
+ }
+ });
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ logMessage("eventLabel='Uploader could not start worker:'" + e.getMessage() + "'");
}
+ // Resend pending ACK at startup (and warmup database)
+
+ ackDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ List uploadEvents = ackDatabase
+ .uploadEventDao()
+ .getAll();
+ int ackDelay = 0;
+ for (UploadEvent ack : uploadEvents) {
+ executorService.schedule(() -> {
+ handleAck(ack.getOutputData());
+ }, ackDelay, TimeUnit.MILLISECONDS);
+ ackDelay += 200;
+ }
+ }
+ });
+
// Can't use observeForever anywhere else than the main thread
cordova.getActivity().runOnUiThread(() -> {
// Listen for upload progress
@@ -202,12 +239,17 @@ private void initManager(String options, final CallbackContext callbackContext)
for (WorkInfo info : tasks) {
// No db in main thread
executorService.schedule(() -> {
- final List uploadEventsList = ackDatabase
- .uploadEventDao()
- .getAll();
- for (UploadEvent ack : uploadEventsList) {
- handleAck(ack.getOutputData());
- }
+ ackDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ List uploadEventsList = ackDatabase
+ .uploadEventDao()
+ .getAll();
+ for (UploadEvent ack : uploadEventsList) {
+ handleAck(ack.getOutputData());
+ }
+ }
+ });
}, 0, TimeUnit.MILLISECONDS);
switch (info.getState()) {
// If the upload in not finished, publish its progress
@@ -226,6 +268,7 @@ private void initManager(String options, final CallbackContext callbackContext)
case SUCCEEDED:
logMessage("Task succeeded: " + info.getId());
completedTasks++;
+ break;
case FAILED:
// The task can't fail completely so something really bad has happened.
logMessage("eventLabel='Uploader failed inexplicably' error='" + info.getOutputData() + "'");
@@ -257,6 +300,10 @@ private void addUpload(JSONObject jsonPayload) {
final String uploadId = String.valueOf(payload.get("id"));
+ if (uploadId == null) {
+ return;
+ }
+
// Create headers
final Map headers;
try {
@@ -325,9 +372,19 @@ private void addUpload(JSONObject jsonPayload) {
)
);
- if (!workerIsStarted) {
- startWorkers();
- workerIsStarted = true;
+ try {
+ List workInfoStates = new ArrayList<>();
+ workInfoStates.add(WorkInfo.State.BLOCKED);
+ workInfoStates.add(WorkInfo.State.CANCELLED);
+ workInfoStates.add(WorkInfo.State.ENQUEUED);
+ workInfoStates.add(WorkInfo.State.RUNNING);
+ List workers = WorkManager.getInstance(cordova.getContext()).getWorkInfos(WorkQuery.Builder.fromStates(workInfoStates).build()).get();
+ if (workers.size() == 0) {
+ startWorkers();
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ logMessage("eventLabel='Uploader could not start worker:'" + e.getMessage() + "'");
}
}
@@ -336,13 +393,13 @@ private void startWorkers() {
for (int i = 0; i < ccUpload; i++) {
OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(UploadTask.class)
- .setConstraints(new Constraints.Builder()
- .setRequiredNetworkType(NetworkType.CONNECTED)
- .build()
- )
- .keepResultsForAtLeast(0, TimeUnit.MILLISECONDS)
- .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.SECONDS)
- .addTag(FileTransferBackground.WORK_TAG_UPLOAD);
+ .setConstraints(new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ )
+ .keepResultsForAtLeast(0, TimeUnit.MILLISECONDS)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
+ .addTag(FileTransferBackground.WORK_TAG_UPLOAD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
@@ -432,16 +489,20 @@ private void handleAck(final Data ackData) {
* Cleanup response file and ACK entry.
*/
private void cleanupUpload(final String uploadId) {
- final UploadEvent ack = AckDatabase.getInstance(cordova.getContext()).uploadEventDao().getById(uploadId);
+ final UploadEvent uploadEventAck = AckDatabase.getInstance(cordova.getContext()).uploadEventDao().getById(uploadId);
// If the upload is done there is an ACK of it, so get file name from there
- if (ack != null) {
- if (ack.getOutputData().getString(UploadTask.KEY_OUTPUT_RESPONSE_FILE) != null) {
- cordova.getContext().deleteFile(ack.getOutputData().getString(UploadTask.KEY_OUTPUT_RESPONSE_FILE));
+ if (uploadEventAck != null) {
+ if (uploadEventAck.getOutputData().getString(UploadTask.KEY_OUTPUT_RESPONSE_FILE) != null) {
+ cordova.getContext().deleteFile(uploadEventAck.getOutputData().getString(UploadTask.KEY_OUTPUT_RESPONSE_FILE));
}
// Also delete it from database
- AckDatabase.getInstance(cordova.getContext()).uploadEventDao().delete(ack);
+ final PendingUpload pendingUploadAck = AckDatabase.getInstance(cordova.getContext()).pendingUploadDao().getById(uploadId);
+ if (pendingUploadAck != null) {
+ AckDatabase.getInstance(cordova.getContext()).pendingUploadDao().delete(pendingUploadAck);
+ }
+ AckDatabase.getInstance(cordova.getContext()).uploadEventDao().delete(uploadEventAck);
} else {
// Otherwise get the data from the task itself
final WorkInfo task;
diff --git a/src/android/PendingUploadDao.java b/src/android/PendingUploadDao.java
index a1450265..e3eb12a0 100644
--- a/src/android/PendingUploadDao.java
+++ b/src/android/PendingUploadDao.java
@@ -5,6 +5,7 @@
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
+import androidx.room.Update;
import java.util.List;
@@ -34,11 +35,26 @@ public interface PendingUploadDao {
@Query("SELECT COUNT(*) FROM pending_upload WHERE state = 'UPLOADED'")
int getCompletedUploadsCount();
- @Query("UPDATE pending_upload SET state = 'PENDING' WHERE ID = :id")
- void markAsPending(final String id);
+ @Query("UPDATE pending_upload SET state = 'PENDING' WHERE state = 'UPLOADING'")
+ void resetUploadingAsPending();
- @Query("UPDATE pending_upload SET state = 'UPLOADED' WHERE ID = :id")
- void markAsUploaded(final String id);
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ default void markAsPending(final String id) {
+ setState(id, "PENDING");
+ }
+
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ default void markAsUploading(final String id) {
+ setState(id, "UPLOADING");
+ }
+
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ default void markAsUploaded(final String id) {
+ setState(id, "UPLOADED");
+ }
+
+ @Query("UPDATE OR REPLACE pending_upload SET state = :state WHERE ID = :id")
+ void setState(final String id, final String state);
default boolean exists(final String id) {
return getCountById(id) > 0;
diff --git a/src/android/UploadForegroundNotification.java b/src/android/UploadForegroundNotification.java
index 386e29c8..35f93fda 100644
--- a/src/android/UploadForegroundNotification.java
+++ b/src/android/UploadForegroundNotification.java
@@ -44,7 +44,7 @@ public static void done(final UUID uuid) {
collectiveProgress.remove(uuid);
}
- static ForegroundInfo getForegroundInfo(final Context context) {
+ static ForegroundInfo getForegroundInfo(final Context context, float progress) {
final long now = System.currentTimeMillis();
// Set to now to ensure other worker will be throttled
final long lastUpdate = lastNotificationUpdateMs.getAndSet(now);
@@ -56,9 +56,7 @@ static ForegroundInfo getForegroundInfo(final Context context) {
return cachedInfo;
}
- float totalProgressStore = (float) (AckDatabase.getInstance(context).pendingUploadDao().getCompletedUploadsCount() / (double) AckDatabase.getInstance(context).pendingUploadDao().getAllCount());
-
- FileTransferBackground.logMessage("eventLabel='getForegroundInfo: general (" + totalProgressStore + ") all (" + collectiveProgress + ")'");
+ FileTransferBackground.logMessage("eventLabel='getForegroundInfo: general (" + (progress * 100) + ") all (" + collectiveProgress + ")'");
Class> mainActivityClass = null;
try {
@@ -82,7 +80,7 @@ static ForegroundInfo getForegroundInfo(final Context context) {
.setSmallIcon(notificationIconRes)
.setColor(Color.rgb(57, 100, 150))
.setOngoing(true)
- .setProgress(100, (int) (totalProgressStore * 100f), false)
+ .setProgress(100, (int) (progress * 100), false)
.setContentIntent(pendingIntent)
.addAction(notificationIconRes, "Open", pendingIntent)
.build();
diff --git a/src/android/UploadNotification.java b/src/android/UploadNotification.java
index 2d2fe2d1..f128d501 100644
--- a/src/android/UploadNotification.java
+++ b/src/android/UploadNotification.java
@@ -41,9 +41,8 @@ public class UploadNotification {
));
}
- public void updateProgress() {
- float totalProgressStore = (float) (AckDatabase.getInstance(context).pendingUploadDao().getCompletedUploadsCount() / (double) AckDatabase.getInstance(context).pendingUploadDao().getAllCount());
- notificationBuilder.setProgress(100, (int) (totalProgressStore * 100f), false);
+ public void updateProgress(float progress) {
+ notificationBuilder.setProgress(100, (int) (progress * 100), false);
notificationManager.notify(UploadNotification.notificationId, notificationBuilder.build());
}
diff --git a/src/android/UploadTask.java b/src/android/UploadTask.java
index f8bbf766..606d2fe2 100644
--- a/src/android/UploadTask.java
+++ b/src/android/UploadTask.java
@@ -93,16 +93,12 @@ public UploadTask(@NonNull Context context, @NonNull WorkerParameters workerPara
super(context, workerParams);
- // Migrating code from 4.0.9 to 4.0.10 - Check if upload comes from another worker and does not exists in table
- String oldUploadTaskId = workerParams.getInputData().getString(KEY_INPUT_ID);
- if (!firstMigrationFlag && oldUploadTaskId != null && AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getById(oldUploadTaskId) == null) {
- FileTransferBackground.logMessage("Migrating upload " + oldUploadTaskId);
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().insert(new PendingUpload(oldUploadTaskId, workerParams.getInputData()));
- FileTransferBackground.logMessage("Retrying migrated upload " + oldUploadTaskId + " after some seconds...");
- firstMigrationFlag = true;
- }
-
- nextPendingUpload = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getFirstPendingEntry();
+ AckDatabase.getInstance(getApplicationContext()).runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ nextPendingUpload = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getFirstPendingEntry();
+ }
+ });
if (httpClient == null) {
httpClient = new OkHttpClient.Builder()
@@ -116,8 +112,6 @@ public UploadTask(@NonNull Context context, @NonNull WorkerParameters workerPara
.build();
}
- httpClient.dispatcher().setMaxRequests(nextPendingUpload.getInputData().getInt(KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS, 2));
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
UploadForegroundNotification.configure(
nextPendingUpload.getInputData().getString(UploadTask.KEY_INPUT_NOTIFICATION_TITLE),
@@ -142,35 +136,17 @@ public Result doWork() {
return Result.retry();
}
- // Migrating code from 4.0.9 to 4.0.10 - Check if upload comes from another worker and does not exists in table
- String oldUploadTaskId = getInputData().getString(KEY_INPUT_ID);
- if (oldUploadTaskId != null && AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getById(oldUploadTaskId) == null && firstMigrationFlag == true) {
- FileTransferBackground.logMessage("Migrating upload " + oldUploadTaskId);
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().insert(new PendingUpload(oldUploadTaskId, getInputData()));
- FileTransferBackground.logMessage("Retrying migrated upload " + oldUploadTaskId + " after some seconds...");
- return Result.success();
- }
-
- do {
- nextPendingUpload = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getFirstPendingEntry();
-
- final String id = nextPendingUpload.getInputData().getString(KEY_INPUT_ID);
-
- if (id == null) {
- FileTransferBackground.logMessageError("doWork: ID is invalid !", null);
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsPending(nextPendingUpload.getId());
- return Result.failure();
- }
+ while (true) {
+ AckDatabase.getInstance(getApplicationContext()).runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ nextPendingUpload = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getFirstPendingEntry();
+ AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsUploading(nextPendingUpload.getId());
+ }
+ });
- // Check retry count
- if (getRunAttemptCount() > MAX_TRIES) {
- return Result.success(new Data.Builder()
- .putString(KEY_OUTPUT_ID, id)
- .putBoolean(KEY_OUTPUT_IS_ERROR, true)
- .putString(KEY_OUTPUT_FAILURE_REASON, "Too many retries")
- .putBoolean(KEY_OUTPUT_FAILURE_CANCELED, false)
- .build()
- );
+ if (nextPendingUpload == null) {
+ break;
}
Request request = null;
@@ -179,7 +155,7 @@ public Result doWork() {
} catch (FileNotFoundException e) {
FileTransferBackground.logMessageError("doWork: File not found !", e);
return Result.success(new Data.Builder()
- .putString(KEY_OUTPUT_ID, id)
+ .putString(KEY_OUTPUT_ID, nextPendingUpload.getId())
.putBoolean(KEY_OUTPUT_IS_ERROR, true)
.putString(KEY_OUTPUT_FAILURE_REASON, "File not found !")
.putBoolean(KEY_OUTPUT_FAILURE_CANCELED, false)
@@ -191,7 +167,7 @@ public Result doWork() {
// Register me
uploadForegroundNotification.progress(getId(), 0f);
- handleNotification();
+ handleNotification(0f);
// Start call
currentCall = httpClient.newCall(request);
@@ -225,13 +201,18 @@ public Result doWork() {
// See #handleProgress for cancel code
if (isStopped()) {
final Data data = new Data.Builder()
- .putString(KEY_OUTPUT_ID, id)
+ .putString(KEY_OUTPUT_ID, nextPendingUpload.getId())
.putBoolean(KEY_OUTPUT_IS_ERROR, true)
.putString(KEY_OUTPUT_FAILURE_REASON, "User cancelled")
.putBoolean(KEY_OUTPUT_FAILURE_CANCELED, true)
.build();
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsUploaded(nextPendingUpload.getId());
- AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(id, data));
+ AckDatabase.getInstance(getApplicationContext()).runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsUploaded(nextPendingUpload.getId());
+ AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(nextPendingUpload.getId(), data));
+ }
+ });
return Result.success(data);
} else {
// But if it was not it must be a connectivity problem or
@@ -246,7 +227,7 @@ public Result doWork() {
// Start building the output data
final Data.Builder outputData = new Data.Builder()
- .putString(KEY_OUTPUT_ID, id)
+ .putString(KEY_OUTPUT_ID, nextPendingUpload.getId())
.putBoolean(KEY_OUTPUT_IS_ERROR, false)
.putInt(KEY_OUTPUT_STATUS_CODE, (!DEBUG_SKIP_UPLOAD) ? response.code() : 200);
@@ -275,17 +256,25 @@ public Result doWork() {
}
final Data data = outputData.build();
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsUploaded(nextPendingUpload.getId());
- AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(id, data));
- } while(AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getPendingUploadsCount() > 0);
-
- final List pendingUploads = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getCompletedUploads();
-
- for (PendingUpload pendingUpload: pendingUploads) {
- AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().delete(pendingUpload);
+ AckDatabase.getInstance(getApplicationContext()).runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().markAsUploaded(nextPendingUpload.getId());
+ AckDatabase.getInstance(getApplicationContext()).uploadEventDao().insert(new UploadEvent(nextPendingUpload.getId(), data));
+ }
+ });
}
- FileTransferBackground.workerIsStarted = false;
+ AckDatabase.getInstance(getApplicationContext()).runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ final List pendingUploads = AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().getCompletedUploads();
+
+ for (PendingUpload pendingUpload:pendingUploads) {
+ AckDatabase.getInstance(getApplicationContext()).pendingUploadDao().delete(pendingUpload);
+ }
+ }
+ });
return Result.success();
}
@@ -303,16 +292,13 @@ private void handleProgress(long bytesWritten, long totalBytes) {
float percent = (float) bytesWritten / (float) totalBytes;
UploadForegroundNotification.progress(getId(), percent);
-
- FileTransferBackground.logMessageInfo("handleProgress: " + getId() + " Progress: " + (int) (percent * 100f));
-
final Data data = new Data.Builder()
.putString(KEY_PROGRESS_ID, nextPendingUpload.getInputData().getString(KEY_INPUT_ID))
.putInt(KEY_PROGRESS_PERCENT, (int) (percent * 100f))
.build();
FileTransferBackground.logMessage("handleProgress: Progress data: " + data);
setProgressAsync(data);
- handleNotification();
+ handleNotification(percent);
}
/**
@@ -388,12 +374,12 @@ private Request createRequest() throws FileNotFoundException {
return requestBuilder.build();
}
- private void handleNotification() {
+ private void handleNotification(float progress) {
FileTransferBackground.logMessage("Upload Notification");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- setForegroundAsync(uploadForegroundNotification.getForegroundInfo(getApplicationContext()));
+ setForegroundAsync(uploadForegroundNotification.getForegroundInfo(getApplicationContext(), progress));
} else {
- uploadNotification.updateProgress();
+ uploadNotification.updateProgress(progress);
}
FileTransferBackground.logMessage("Upload Notification Exit");
}