Skip to content

Commit 269c763

Browse files
authored
Merge pull request #226 from spoonconsulting/one-running-worker
Manage WorkManager: One Running Worker to upload a Batch of Files
2 parents a97c5ac + 24e686c commit 269c763

File tree

9 files changed

+356
-314
lines changed

9 files changed

+356
-314
lines changed

plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
<source-file src="src/android/AckDatabase.java" target-dir="src/com/spoon/backgroundfileupload" />
4848
<source-file src="src/android/UploadEventDao.java" target-dir="src/com/spoon/backgroundfileupload" />
4949
<source-file src="src/android/UploadEvent.java" target-dir="src/com/spoon/backgroundfileupload" />
50+
<source-file src="src/android/PendingUploadDao.java" target-dir="src/com/spoon/backgroundfileupload" />
51+
<source-file src="src/android/PendingUpload.java" target-dir="src/com/spoon/backgroundfileupload" />
5052
<source-file src="src/android/ProgressRequestBody.java" target-dir="src/com/spoon/backgroundfileupload" />
5153
<source-file src="src/android/UploadForegroundNotification.java" target-dir="src/com/spoon/backgroundfileupload" />
5254
<source-file src="src/android/UploadNotification.java" target-dir="src/com/spoon/backgroundfileupload" />

src/android/AckDatabase.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import androidx.room.TypeConverters;
99
import androidx.work.Data;
1010

11-
@Database(entities = {UploadEvent.class}, version = 3)
11+
@Database(entities = {UploadEvent.class, PendingUpload.class}, version = 4)
1212
@TypeConverters(value = {Data.class})
1313
public abstract class AckDatabase extends RoomDatabase {
1414
private static AckDatabase instance;
@@ -30,4 +30,6 @@ public static void closeInstance() {
3030
}
3131

3232
public abstract UploadEventDao uploadEventDao();
33+
34+
public abstract PendingUploadDao pendingUploadDao();
3335
}

src/android/FileTransferBackground.java

Lines changed: 77 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import androidx.work.OutOfQuotaPolicy;
1818
import androidx.work.WorkInfo;
1919
import androidx.work.WorkManager;
20-
import androidx.work.WorkQuery;
2120

2221
import org.apache.cordova.CallbackContext;
2322
import org.apache.cordova.CordovaPlugin;
@@ -30,33 +29,29 @@
3029
import java.io.IOException;
3130
import java.io.InputStreamReader;
3231
import java.util.ArrayList;
33-
import java.util.Arrays;
34-
import java.util.Collections;
3532
import java.util.HashMap;
3633
import java.util.Iterator;
3734
import java.util.List;
3835
import java.util.Map;
39-
import java.util.UUID;
4036
import java.util.concurrent.ExecutionException;
4137
import java.util.concurrent.Executors;
4238
import java.util.concurrent.ScheduledExecutorService;
4339
import java.util.concurrent.TimeUnit;
4440

4541
public class FileTransferBackground extends CordovaPlugin {
46-
47-
private static final String TAG = "FileTransferBackground";
4842
public static final String WORK_TAG_UPLOAD = "work_tag_upload";
4943

5044
private CallbackContext uploadCallback;
5145
private boolean ready = false;
5246

5347
private Data httpClientBaseConfig = Data.EMPTY;
5448

55-
private static String currentTag;
56-
private static long currentTagFetchedAt;
49+
public static boolean workerIsStarted;
5750

5851
private ScheduledExecutorService executorService = null;
5952

53+
private int ccUpload;
54+
6055
public void sendCallback(JSONObject obj) {
6156
/* we check the webview has been initialized */
6257
if (ready) {
@@ -163,8 +158,7 @@ private void initManager(String options, final CallbackContext callbackContext)
163158

164159
try {
165160
final JSONObject settings = new JSONObject(options);
166-
int ccUpload = settings.getInt("parallelUploadsLimit");
167-
161+
ccUpload = settings.getInt("parallelUploadsLimit");
168162
// Rebuild base HTTP config
169163
httpClientBaseConfig = new Data.Builder()
170164
.putInt(UploadTask.KEY_INPUT_CONFIG_CONCURRENT_DOWNLOADS, ccUpload)
@@ -206,31 +200,32 @@ private void initManager(String options, final CallbackContext callbackContext)
206200
.observeForever((tasks) -> {
207201
int completedTasks = 0;
208202
for (WorkInfo info : tasks) {
203+
// No db in main thread
204+
executorService.schedule(() -> {
205+
final List<UploadEvent> uploadEventsList = ackDatabase
206+
.uploadEventDao()
207+
.getAll();
208+
for (UploadEvent ack : uploadEventsList) {
209+
handleAck(ack.getOutputData());
210+
}
211+
}, 0, TimeUnit.MILLISECONDS);
209212
switch (info.getState()) {
210213
// If the upload in not finished, publish its progress
211214
case RUNNING:
212215
if (info.getProgress() != Data.EMPTY) {
213216
String id = info.getProgress().getString(UploadTask.KEY_PROGRESS_ID);
214217
int progress = info.getProgress().getInt(UploadTask.KEY_PROGRESS_PERCENT, 0);
215218

216-
Log.d(TAG, "initManager: " + info.getId() + " (" + info.getState() + ") Progress: " + progress);
219+
logMessage("initManager: " + info.getId() + " (" + info.getState() + ") Progress: " + progress);
217220
sendProgress(id, progress);
218221
}
219222
break;
220223
case CANCELLED:
221224
case BLOCKED:
222225
case ENQUEUED:
223226
case SUCCEEDED:
227+
logMessage("Task succeeded: " + info.getId());
224228
completedTasks++;
225-
// No db in main thread
226-
executorService.schedule(() -> {
227-
// The corresponding ACK is already in the DB, if it not, the task is just a leftover
228-
String id = info.getOutputData().getString(UploadTask.KEY_OUTPUT_ID);
229-
if (ackDatabase.uploadEventDao().exists(id)) {
230-
handleAck(info.getOutputData());
231-
}
232-
}, 0, TimeUnit.MILLISECONDS);
233-
break;
234229
case FAILED:
235230
// The task can't fail completely so something really bad has happened.
236231
logMessage("eventLabel='Uploader failed inexplicably' error='" + info.getOutputData() + "'");
@@ -300,59 +295,67 @@ private void addUpload(JSONObject jsonPayload) {
300295
} catch(PackageManager.NameNotFoundException e) {
301296
e.printStackTrace();
302297
}
303-
startUpload(uploadId, new Data.Builder()
304-
// Put base info
305-
.putString(UploadTask.KEY_INPUT_ID, uploadId)
306-
.putString(UploadTask.KEY_INPUT_URL, (String) payload.get("serverUrl"))
307-
.putString(UploadTask.KEY_INPUT_FILEPATH, (String) payload.get("filePath"))
308-
.putString(UploadTask.KEY_INPUT_FILE_KEY, (String) payload.get("fileKey"))
309-
.putString(UploadTask.KEY_INPUT_HTTP_METHOD, (String) payload.get("requestMethod"))
310-
311-
// Put headers
312-
.putInt(UploadTask.KEY_INPUT_HEADERS_COUNT, headersNames.size())
313-
.putStringArray(UploadTask.KEY_INPUT_HEADERS_NAMES, headersNames.toArray(new String[0]))
314-
.putAll(headerValues)
315-
316-
// Put query parameters
317-
.putInt(UploadTask.KEY_INPUT_PARAMETERS_COUNT, parameterNames.size())
318-
.putStringArray(UploadTask.KEY_INPUT_PARAMETERS_NAMES, parameterNames.toArray(new String[0]))
319-
.putAll(parameterValues)
320-
321-
// Put notification stuff
322-
.putString(UploadTask.KEY_INPUT_NOTIFICATION_TITLE, (String) payload.get("notificationTitle"))
323-
.putString(UploadTask.KEY_INPUT_NOTIFICATION_ICON, cordova.getActivity().getPackageName() + ":drawable/ic_upload")
324-
.putString(UploadTask.KEY_INPUT_CONFIG_INTENT_ACTIVITY, intentActivity)
325-
326-
// Put config stuff
327-
.putAll(httpClientBaseConfig)
328-
.build()
298+
299+
AckDatabase.getInstance(cordova.getContext()).pendingUploadDao().insert(
300+
new PendingUpload(
301+
uploadId,
302+
new Data.Builder()
303+
// Put base info
304+
.putString(UploadTask.KEY_INPUT_ID, uploadId)
305+
.putString(UploadTask.KEY_INPUT_URL, (String) payload.get("serverUrl"))
306+
.putString(UploadTask.KEY_INPUT_FILEPATH, (String) payload.get("filePath"))
307+
.putString(UploadTask.KEY_INPUT_FILE_KEY, (String) payload.get("fileKey"))
308+
.putString(UploadTask.KEY_INPUT_HTTP_METHOD, (String) payload.get("requestMethod"))
309+
// Put headers
310+
.putInt(UploadTask.KEY_INPUT_HEADERS_COUNT, headersNames.size())
311+
.putStringArray(UploadTask.KEY_INPUT_HEADERS_NAMES, headersNames.toArray(new String[0]))
312+
.putAll(headerValues)
313+
// Put query parameters
314+
.putInt(UploadTask.KEY_INPUT_PARAMETERS_COUNT, parameterNames.size())
315+
.putStringArray(UploadTask.KEY_INPUT_PARAMETERS_NAMES, parameterNames.toArray(new String[0]))
316+
.putAll(parameterValues)
317+
// Put notification stuff
318+
.putString(UploadTask.KEY_INPUT_NOTIFICATION_TITLE, (String) payload.get("notificationTitle"))
319+
.putString(UploadTask.KEY_INPUT_NOTIFICATION_ICON, cordova.getActivity().getPackageName() + ":drawable/ic_upload")
320+
.putString(UploadTask.KEY_INPUT_CONFIG_INTENT_ACTIVITY, intentActivity)
321+
322+
// Put config stuff
323+
.putAll(httpClientBaseConfig)
324+
.build()
325+
)
329326
);
330-
}
331327

332-
private void startUpload(final String uploadId, final Data payload) {
333-
Log.d(TAG, "startUpload: Starting work via work manager");
334-
335-
OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(UploadTask.class)
336-
.setConstraints(new Constraints.Builder()
337-
.setRequiredNetworkType(NetworkType.CONNECTED)
338-
.build()
339-
)
340-
.keepResultsForAtLeast(0, TimeUnit.MILLISECONDS)
341-
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.SECONDS)
342-
.addTag(FileTransferBackground.WORK_TAG_UPLOAD)
343-
.addTag(getCurrentTag(cordova.getContext()))
344-
.setInputData(payload);
345-
346-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
347-
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
328+
if (!workerIsStarted) {
329+
startWorkers();
330+
workerIsStarted = true;
348331
}
332+
}
349333

350-
OneTimeWorkRequest workRequest = workRequestBuilder.build();
334+
private void startWorkers() {
335+
logMessage("startUpload: Starting worker via work manager");
336+
337+
for (int i = 0; i < ccUpload; i++) {
338+
OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(UploadTask.class)
339+
.setConstraints(new Constraints.Builder()
340+
.setRequiredNetworkType(NetworkType.CONNECTED)
341+
.build()
342+
)
343+
.keepResultsForAtLeast(0, TimeUnit.MILLISECONDS)
344+
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.SECONDS)
345+
.addTag(FileTransferBackground.WORK_TAG_UPLOAD);
346+
347+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
348+
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
349+
}
351350

352-
WorkManager.getInstance(cordova.getContext())
353-
.enqueueUniqueWork(uploadId, ExistingWorkPolicy.APPEND, workRequest);
351+
OneTimeWorkRequest workRequest = workRequestBuilder.build();
352+
353+
WorkManager.getInstance(cordova.getContext())
354+
.enqueueUniqueWork(FileTransferBackground.WORK_TAG_UPLOAD + "_" + i, ExistingWorkPolicy.KEEP, workRequest);
355+
356+
logMessage("eventLabel=Uploader starting uploads via worker" + i);
357+
}
354358

355-
logMessage("eventLabel='Uploader starting upload' uploadId='" + uploadId + "'");
356359
}
357360

358361
private void sendAddingUploadError(String uploadId, Exception error) {
@@ -430,6 +433,7 @@ private void handleAck(final Data ackData) {
430433
*/
431434
private void cleanupUpload(final String uploadId) {
432435
final UploadEvent ack = AckDatabase.getInstance(cordova.getContext()).uploadEventDao().getById(uploadId);
436+
433437
// If the upload is done there is an ACK of it, so get file name from there
434438
if (ack != null) {
435439
if (ack.getOutputData().getString(UploadTask.KEY_OUTPUT_RESPONSE_FILE) != null) {
@@ -501,44 +505,15 @@ public static HashMap<String, Object> convertToHashMap(JSONObject jsonObject) th
501505
return hashMap;
502506
}
503507

504-
public static String getCurrentTag(Context context) {
505-
final long now = System.currentTimeMillis();
506-
if (currentTag != null && now - currentTagFetchedAt <= 5000) {
507-
return currentTag;
508-
}
509-
currentTagFetchedAt = now;
510-
currentTag = fetchCurrentTag(context);
511-
return currentTag;
508+
public static void logMessage(String message) {
509+
Log.d("CordovaBackgroundUpload", message);
512510
}
513511

514-
public static String fetchCurrentTag(Context context) {
515-
WorkQuery workQuery = WorkQuery.Builder
516-
.fromTags(Arrays.asList(FileTransferBackground.WORK_TAG_UPLOAD))
517-
.addStates(Arrays.asList(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
518-
.build();
519-
List<WorkInfo> workInfo;
520-
try {
521-
workInfo = WorkManager.getInstance(context)
522-
.getWorkInfos(workQuery)
523-
.get();
524-
} catch (ExecutionException | InterruptedException e) {
525-
Log.w(TAG, "getForegroundInfo: Problem while retrieving task list:", e);
526-
workInfo = Collections.emptyList();
527-
}
528-
String prefix = "packet_";
529-
for (WorkInfo info : workInfo) {
530-
if (!info.getState().isFinished()) {
531-
for (String tag : info.getTags()) {
532-
if (tag.startsWith(prefix)) {
533-
return tag;
534-
}
535-
}
536-
}
537-
}
538-
return prefix + UUID.randomUUID().toString();
512+
public static void logMessageInfo(String message) {
513+
Log.i("CordovaBackgroundUpload", message);
539514
}
540515

541-
public static void logMessage(String message) {
542-
Log.d("CordovaBackgroundUpload", message);
516+
public static void logMessageError(String message, Exception exception) {
517+
Log.e("CordovaBackgroundUpload", message, exception);
543518
}
544519
}

src/android/PendingUpload.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.spoon.backgroundfileupload;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.room.ColumnInfo;
5+
import androidx.room.Entity;
6+
import androidx.room.PrimaryKey;
7+
import androidx.work.Data;
8+
9+
@Entity(tableName = "pending_upload")
10+
public class PendingUpload {
11+
@PrimaryKey
12+
@NonNull
13+
private String id;
14+
15+
@ColumnInfo(name = "output_data")
16+
@NonNull
17+
private Data inputData;
18+
19+
@ColumnInfo(name = "state")
20+
@NonNull
21+
private String state;
22+
23+
public PendingUpload(@NonNull final String id, @NonNull final Data inputData) {
24+
this.id = id;
25+
this.inputData = inputData;
26+
this.state = "PENDING";
27+
}
28+
29+
@NonNull
30+
public void setState(@NonNull final String state) {
31+
this.state = state;
32+
}
33+
34+
@NonNull
35+
public String getId() {
36+
return id;
37+
}
38+
39+
@NonNull
40+
public Data getInputData() {
41+
return inputData;
42+
}
43+
44+
@NonNull
45+
public String getState() {
46+
return state;
47+
}
48+
}

0 commit comments

Comments
 (0)