Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
</engines>
<dependency id="cordova-plugin-file" version="6.0.2"/>
<platform name="android">
<framework src="androidx.work:work-runtime:2.8.0" />
<framework src="androidx.work:work-runtime:2.8.1" />
<framework src="com.squareup.okhttp3:okhttp:4.9.3" />
<framework src="androidx.room:room-runtime:2.5.0" />
<framework src="androidx.room:room-runtime:2.5.1" />
<framework src="src/android/config.gradle" custom="true" type="gradleReference"/>
<config-file target="res/xml/config.xml" parent="/*">
<feature name="FileTransferBackground">
Expand Down
129 changes: 95 additions & 34 deletions src/android/FileTransferBackground.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<UploadEvent> 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<WorkInfo.State> workInfoStates = new ArrayList<>();
workInfoStates.add(WorkInfo.State.BLOCKED);
workInfoStates.add(WorkInfo.State.CANCELLED);
workInfoStates.add(WorkInfo.State.ENQUEUED);
workInfoStates.add(WorkInfo.State.RUNNING);
List<WorkInfo> 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<UploadEvent> 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
Expand All @@ -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<UploadEvent> uploadEventsList = ackDatabase
.uploadEventDao()
.getAll();
for (UploadEvent ack : uploadEventsList) {
handleAck(ack.getOutputData());
}
ackDatabase.runInTransaction(new Runnable() {
@Override
public void run() {
List<UploadEvent> 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
Expand All @@ -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() + "'");
Expand Down Expand Up @@ -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<String, Object> headers;
try {
Expand Down Expand Up @@ -325,9 +372,19 @@ private void addUpload(JSONObject jsonPayload) {
)
);

if (!workerIsStarted) {
startWorkers();
workerIsStarted = true;
try {
List<WorkInfo.State> workInfoStates = new ArrayList<>();
workInfoStates.add(WorkInfo.State.BLOCKED);
workInfoStates.add(WorkInfo.State.CANCELLED);
workInfoStates.add(WorkInfo.State.ENQUEUED);
workInfoStates.add(WorkInfo.State.RUNNING);
List<WorkInfo> 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() + "'");
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 20 additions & 4 deletions src/android/PendingUploadDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

import java.util.List;

Expand Down Expand Up @@ -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;
Expand Down
8 changes: 3 additions & 5 deletions src/android/UploadForegroundNotification.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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();
Expand Down
5 changes: 2 additions & 3 deletions src/android/UploadNotification.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Loading