Skip to content

Commit 74ceaa9

Browse files
authored
Fix Upload issue using WorkManager
1 parent 5f61b6c commit 74ceaa9

File tree

3 files changed

+135
-77
lines changed

3 files changed

+135
-77
lines changed

core/src/androidTest/java/com/cloudinary/android/AndroidJobStrategyTest.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,36 @@
1616
import org.junit.Test;
1717
import org.junit.runner.RunWith;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.lang.reflect.Field;
2122
import java.lang.reflect.ParameterizedType;
2223

2324
@RunWith(AndroidJUnit4ClassRunner.class)
2425
public class AndroidJobStrategyTest extends AbstractTest {
2526

27+
private File payloadFile;
28+
29+
@Override
30+
protected void finalize() throws Throwable {
31+
super.finalize();
32+
33+
if(payloadFile != null) {
34+
//noinspection ResultOfMethodCallIgnored
35+
payloadFile.delete();
36+
}
37+
}
38+
2639
@Test
2740
public void testAdapter() throws InterruptedException, IOException, NoSuchFieldException, IllegalAccessException {
2841
FilePayload payload = buildPayload();
2942

3043
int tenMinutes = 10 * 60 * 1000;
3144
UploadRequest<FilePayload> request = buildUploadRequest(payload, tenMinutes);
3245

33-
WorkRequest adapted = AndroidJobStrategy.adapt(request);
46+
payloadFile = File.createTempFile("payload", request.getRequestId());
47+
48+
WorkRequest adapted = AndroidJobStrategy.adapt(request, payloadFile);
3449
Class obj = adapted.getClass().getSuperclass();
3550
Field field = obj.getDeclaredField("mWorkSpec");
3651
field.setAccessible(true);
Lines changed: 60 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,49 @@
11
package com.cloudinary.android;
22

33
import android.content.Context;
4-
import android.os.PowerManager;
4+
import android.os.Bundle;
55

66
import androidx.annotation.NonNull;
7-
import androidx.lifecycle.LiveData;
87
import androidx.work.BackoffPolicy;
98
import androidx.work.Constraints;
109
import androidx.work.Data;
10+
import androidx.work.ExistingWorkPolicy;
1111
import androidx.work.NetworkType;
1212
import androidx.work.OneTimeWorkRequest;
1313
import androidx.work.Operation;
1414
import androidx.work.WorkInfo;
1515
import androidx.work.WorkManager;
16-
import androidx.work.WorkRequest;
1716
import androidx.work.Worker;
1817
import androidx.work.WorkerParameters;
1918

2019
import com.cloudinary.android.callback.UploadStatus;
2120
import com.cloudinary.android.policy.UploadPolicy;
2221

23-
import java.lang.ref.WeakReference;
22+
import java.io.File;
23+
import java.io.FileInputStream;
24+
import java.io.IOException;
25+
import java.io.ObjectInputStream;
2426
import java.util.List;
25-
import java.util.Map;
26-
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.TimeUnit;
2828

2929
public class AndroidJobStrategy implements BackgroundRequestStrategy {
3030

3131
private static final String JOB_TAG = "CLD";
3232

33-
private static final Map<String, WeakReference<Thread>> threads = new ConcurrentHashMap<>();
34-
private static final Object threadsMapLockObject = new Object();
3533
private Context context;
3634

37-
public static WorkRequest adapt(UploadRequest request) {
35+
public static OneTimeWorkRequest adapt(UploadRequest<?> request, File payloadFile) {
3836
UploadPolicy policy = request.getUploadPolicy();
3937

40-
Constraints.Builder constraintsBuilder = new Constraints.Builder()
41-
.setRequiredNetworkType(adaptNetworkType(policy.getNetworkType()))
42-
.setRequiresCharging(policy.isRequiresCharging());
38+
Constraints.Builder constraintsBuilder = new Constraints.Builder().setRequiredNetworkType(adaptNetworkType(policy.getNetworkType())).setRequiresCharging(policy.isRequiresCharging());
4339
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
4440
constraintsBuilder.setRequiresDeviceIdle(policy.isRequiresIdle());
4541
}
4642
Constraints constraints = constraintsBuilder.build();
4743

48-
Data inputData = request.buildPayload();
44+
Data inputData = request.buildPayload(payloadFile);
4945

50-
OneTimeWorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadJob.class)
51-
.setBackoffCriteria(adaptBackoffPolicy(policy.getBackoffPolicy()),policy.getBackoffMillis(), TimeUnit.MILLISECONDS)
52-
.setInputData(inputData)
53-
.setConstraints(constraints)
54-
.addTag(JOB_TAG)
55-
.build();
56-
return uploadWorkRequest;
46+
return new OneTimeWorkRequest.Builder(UploadJob.class).setBackoffCriteria(adaptBackoffPolicy(policy.getBackoffPolicy()), policy.getBackoffMillis(), TimeUnit.MILLISECONDS).setInputData(inputData).setConstraints(constraints).addTag(JOB_TAG).build();
5747
}
5848

5949
private static BackoffPolicy adaptBackoffPolicy(UploadPolicy.BackoffPolicy backoffPolicy) {
@@ -85,9 +75,20 @@ public void init(Context context) {
8575
}
8676

8777
@Override
88-
public void doDispatch(UploadRequest request) {
89-
WorkRequest uploadWorkRequest = adapt(request);
90-
WorkManager.getInstance().enqueue(uploadWorkRequest);
78+
public void doDispatch(@SuppressWarnings("rawtypes") @NonNull UploadRequest request) {
79+
File cacheDir = context.getCacheDir();
80+
try {
81+
// Prepare payload file placeholder to temporarily store payload data.
82+
File payloadFile = File.createTempFile("payload", request.getRequestId(), cacheDir);
83+
OneTimeWorkRequest uploadWorkRequest = adapt(request, payloadFile);
84+
WorkManager.getInstance(context).beginUniqueWork(
85+
// Use request ID as unique work name
86+
request.getRequestId(),
87+
// If work already exist, do nothing.
88+
ExistingWorkPolicy.KEEP, uploadWorkRequest).enqueue();
89+
} catch (IOException e) {
90+
e.printStackTrace();
91+
}
9192
}
9293

9394
@Override
@@ -97,13 +98,13 @@ public void executeRequestsNow(int howMany) {
9798

9899
@Override
99100
public boolean cancelRequest(String requestId) {
100-
Operation operation = WorkManager.getInstance().cancelAllWorkByTag(requestId);
101+
Operation operation = WorkManager.getInstance(context).cancelAllWorkByTag(requestId);
101102
return operation.getResult().isCancelled();
102103
}
103104

104105
@Override
105106
public int cancelAllRequests() {
106-
WorkManager.getInstance().cancelAllWork();
107+
WorkManager.getInstance(context).cancelAllWork();
107108
return 0;
108109
}
109110

@@ -119,7 +120,7 @@ public int getRunningJobsCount() {
119120

120121
private int getJobCountByState(WorkInfo.State state) {
121122
int counter = 0;
122-
List<WorkInfo> list = WorkManager.getInstance().getWorkInfosByTagLiveData(JOB_TAG).getValue();
123+
List<WorkInfo> list = WorkManager.getInstance(context).getWorkInfosByTagLiveData(JOB_TAG).getValue();
123124
if (list != null) {
124125
for (WorkInfo info : list) {
125126
if (info.getState() == state) {
@@ -133,9 +134,8 @@ private int getJobCountByState(WorkInfo.State state) {
133134

134135
public static final class UploadJob extends Worker {
135136

136-
private Context context;
137-
private String requestId;
138-
private WorkerParameters workParams;
137+
private final Context context;
138+
private final WorkerParameters workParams;
139139

140140
public UploadJob(@NonNull Context context, @NonNull WorkerParameters workerParams) {
141141
super(context, workerParams);
@@ -146,35 +146,27 @@ public UploadJob(@NonNull Context context, @NonNull WorkerParameters workerParam
146146
@NonNull
147147
@Override
148148
public Result doWork() {
149-
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
150-
final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CLD:UPLOADER");
151-
requestId = workParams.getInputData().getString("requestId");
152-
registerThread();
149+
// Removed Wakelock logic as it causes RuntimeException ("WakeLock under-locked")
153150

154-
wl.acquire();
155-
try {
151+
// Prepare extract payload data from temporary file.
152+
String payloadFilePath = workParams.getInputData().getString(UploadRequest.PayloadData.KEY);
153+
if (payloadFilePath == null) {
154+
// NO Payload input file created prior to request.
155+
return Result.failure();
156+
}
157+
File payloadFile = new File(payloadFilePath);
158+
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(payloadFile))) {
159+
UploadRequest.PayloadData payloadData = (UploadRequest.PayloadData) ois.readObject();
160+
AndroidJobStrategy.AndroidJobRequestParams jobInputData = new AndroidJobStrategy.AndroidJobRequestParams(payloadData);
156161

157162
// call the generic processor:
158-
UploadStatus result = MediaManager.get().processRequest(context, new AndroidJobStrategy.AndroidJobRequestParams(workParams.getInputData()));
163+
UploadStatus result = MediaManager.get().processRequest(context, jobInputData);
159164
return adaptResult(result);
160-
} finally {
161-
wl.release();
162-
unregisterThread();
163-
}
164-
}
165-
166-
private void registerThread() {
167-
synchronized (threadsMapLockObject) {
168-
threads.put(requestId, new WeakReference<>(Thread.currentThread()));
169-
}
170-
}
171165

172-
private void unregisterThread() {
173-
synchronized (threadsMapLockObject) {
174-
WeakReference<Thread> removed = threads.remove(requestId);
175-
if (removed != null) {
176-
removed.clear();
177-
}
166+
} catch (NullPointerException | IOException | ClassNotFoundException e) {
167+
// Unable to deserialize payload data from file.
168+
e.printStackTrace();
169+
return Result.failure();
178170
}
179171
}
180172

@@ -194,54 +186,54 @@ private Result adaptResult(UploadStatus res) {
194186
}
195187

196188
private static final class AndroidJobRequestParams implements RequestParams {
197-
private final Data bundle;
189+
private final Bundle data;
198190

199-
private AndroidJobRequestParams(Data bundle) {
200-
this.bundle = bundle;
191+
private AndroidJobRequestParams(UploadRequest.PayloadData payloadData) {
192+
this.data = new Bundle();
193+
this.data.putString("uri", payloadData.getUri());
194+
this.data.putString("requestId", payloadData.getRequestId());
195+
this.data.putInt("maxErrorRetries", payloadData.getMaxErrorRetries());
196+
this.data.putString("options", payloadData.getOptions());
201197
}
202198

203199
@Override
204200
public void putString(String key, String value) {
205-
putIntoExistingDataObject().putString(key, value).build();
201+
data.putString(key, value);
206202
}
207203

208204
@Override
209205
public void putInt(String key, int value) {
210-
putIntoExistingDataObject().putInt(key, value).build();
206+
data.putInt(key, value);
211207
}
212208

213209
@Override
214210
public void putLong(String key, long value) {
215-
putIntoExistingDataObject().putLong(key, value).build();
211+
data.putLong(key, value);
216212
}
217213

218214
@Override
219215
public void putBoolean(String key, boolean value) {
220-
putIntoExistingDataObject().putBoolean(key, value).build();
216+
data.putBoolean(key, value);
221217
}
222218

223219
@Override
224220
public String getString(String key, String defaultValue) {
225-
return (bundle.getString(key) != null) ? bundle.getString(key) : defaultValue;
221+
return (data.getString(key) != null) ? data.getString(key) : defaultValue;
226222
}
227223

228224
@Override
229225
public int getInt(String key, int defaultValue) {
230-
return bundle.getInt(key, defaultValue);
226+
return data.getInt(key, defaultValue);
231227
}
232228

233229
@Override
234230
public long getLong(String key, long defaultValue) {
235-
return bundle.getLong(key, defaultValue);
231+
return data.getLong(key, defaultValue);
236232
}
237233

238234
@Override
239235
public boolean getBoolean(String key, boolean defaultValue) {
240-
return bundle.getBoolean(key, defaultValue);
241-
}
242-
243-
private Data.Builder putIntoExistingDataObject() {
244-
return new Data.Builder().putAll(bundle);
236+
return data.getBoolean(key, defaultValue);
245237
}
246238
}
247239
}

core/src/main/java/com/cloudinary/android/UploadRequest.java

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cloudinary.android;
22

33
import android.content.Context;
4+
45
import androidx.annotation.NonNull;
56
import androidx.annotation.Nullable;
67
import androidx.work.Data;
@@ -18,7 +19,11 @@
1819
import com.cloudinary.android.preprocess.ResourceCreationException;
1920
import com.cloudinary.utils.ObjectUtils;
2021

22+
import java.io.File;
23+
import java.io.FileOutputStream;
2124
import java.io.IOException;
25+
import java.io.ObjectOutputStream;
26+
import java.io.Serializable;
2227
import java.util.HashMap;
2328
import java.util.Map;
2429
import java.util.UUID;
@@ -334,14 +339,22 @@ void populateParamsFromFields(RequestParams target) {
334339
target.putString("options", getOptionsString());
335340
}
336341

337-
public Data buildPayload() {
338-
Data data = new Data.Builder()
339-
.putString("uri", getPayload().toUri())
340-
.putString("requestId", getRequestId())
341-
.putInt("maxErrorRetries", getUploadPolicy().getMaxErrorRetries())
342-
.putString("options", getOptionsString())
343-
.build();
344-
return data;
342+
public Data buildPayload(File payloadFile) {
343+
Data.Builder dataBuilder = new Data.Builder();
344+
345+
/*
346+
* Store Payload data on temporary file in preparation for [{@link com.cloudinary.android.AndroidJobStrategy.UploadJob}].
347+
*/
348+
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(payloadFile))) {
349+
PayloadData data = new PayloadData(getPayload().toUri(), getRequestId(), getUploadPolicy().getMaxErrorRetries(), getOptionsString());
350+
oos.writeObject(data);
351+
} catch (IOException e) {
352+
e.printStackTrace();
353+
}
354+
355+
dataBuilder.putString(PayloadData.KEY, payloadFile.getAbsolutePath());
356+
357+
return dataBuilder.build();
345358
}
346359

347360
/**
@@ -381,4 +394,42 @@ public void onReschedule(String requestId, ErrorInfo error) {
381394
callback.onReschedule(requestId, error);
382395
}
383396
}
397+
398+
public static class PayloadData implements Serializable {
399+
400+
public transient final static String KEY = "payload_file_path";
401+
402+
private final String uri;
403+
private final String requestId;
404+
private final int maxErrorRetries;
405+
private final String options;
406+
407+
public PayloadData() {
408+
this(null, null, 1, null);
409+
}
410+
411+
public PayloadData(String uri, String requestId, int maxErrorRetries, String options) {
412+
this.uri = uri;
413+
this.requestId = requestId;
414+
this.maxErrorRetries = maxErrorRetries;
415+
this.options = options;
416+
}
417+
418+
public String getUri() {
419+
return uri;
420+
}
421+
422+
public String getRequestId() {
423+
return requestId;
424+
}
425+
426+
public int getMaxErrorRetries() {
427+
return maxErrorRetries;
428+
}
429+
430+
public String getOptions() {
431+
return options;
432+
}
433+
434+
}
384435
}

0 commit comments

Comments
 (0)