diff --git a/src/android/AckDatabase.java b/src/android/AckDatabase.java index ac4cc3ed..b6173f3d 100644 --- a/src/android/AckDatabase.java +++ b/src/android/AckDatabase.java @@ -2,22 +2,36 @@ import android.content.Context; +import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.work.Data; -@Database(entities = {UploadEvent.class}, version = 5) +@Database(entities = {UploadEvent.class}, version = 6) @TypeConverters(value = {Data.class}) public abstract class AckDatabase extends RoomDatabase { private static AckDatabase instance; - public static AckDatabase getInstance(final Context context) { + static final Migration MIGRATION_5_6 = new Migration(5, 6) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE upload_events ADD COLUMN uploadDuration INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE upload_events ADD COLUMN startUploadTime INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE upload_events ADD COLUMN finishUploadTime INTEGER NOT NULL DEFAULT 0"); + } + }; + + + public static synchronized AckDatabase getInstance(final Context context) { if (instance == null) { instance = Room .databaseBuilder(context, AckDatabase.class, "cordova-plugin-background-upload.db") .fallbackToDestructiveMigration() + .addMigrations(MIGRATION_5_6) .build(); } return instance; diff --git a/src/android/FileTransferBackground.java b/src/android/FileTransferBackground.java index 8cb6c393..ed8d62f1 100644 --- a/src/android/FileTransferBackground.java +++ b/src/android/FileTransferBackground.java @@ -81,7 +81,7 @@ private void sendProgress(final String id, int progressPercent) { } } - private void sendSuccess(final String id, final String response, int statusCode) { + private void sendSuccess(final String id, final String response, int statusCode, long uploadDuration, long startUploadTime, long finishUploadTime) { if (response != null && !response.isEmpty()) { logMessage("eventLabel='Uploader onSuccess' uploadId='" + id + "' response='" + response.substring(0, Math.min(2000, response.length() - 1)) + "'"); } else { @@ -95,6 +95,9 @@ private void sendSuccess(final String id, final String response, int statusCode) .put("state", "UPLOADED") .put("serverResponse", response) .put("statusCode", statusCode) + .put("uploadDuration", uploadDuration) + .put("startUploadTime", startUploadTime) + .put("finishUploadTime", finishUploadTime) ); } catch (JSONException e) { // Can't really happen but just in case @@ -412,7 +415,10 @@ private void handleAck(final Data ackData) { sendSuccess( ackData.getString(UploadTask.KEY_OUTPUT_ID), response, - ackData.getInt(UploadTask.KEY_OUTPUT_STATUS_CODE, -1 /* If this is sent, something is really wrong */) + ackData.getInt(UploadTask.KEY_OUTPUT_STATUS_CODE, -1 /* If this is sent, something is really wrong */), + ackData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_DURATION, 0), + ackData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_START_TIME, 0), + ackData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_END_TIME, 0) ); } else { diff --git a/src/android/UploadEvent.java b/src/android/UploadEvent.java index 33cf12f7..1ad87e92 100644 --- a/src/android/UploadEvent.java +++ b/src/android/UploadEvent.java @@ -11,6 +11,10 @@ public class UploadEvent { @PrimaryKey @NonNull private String id; + private long uploadDuration; + private long startUploadTime; + private long finishUploadTime; + @ColumnInfo(name = "output_data") @NonNull @@ -19,6 +23,9 @@ public class UploadEvent { public UploadEvent(@NonNull final String id, @NonNull final Data outputData) { this.id = id; this.outputData = outputData; + this.uploadDuration = outputData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_DURATION, 0); + this.startUploadTime = outputData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_START_TIME, 0); + this.finishUploadTime = outputData.getLong(UploadTask.KEY_OUTPUT_UPLOAD_END_TIME, 0); } @NonNull @@ -30,4 +37,28 @@ public String getId() { public Data getOutputData() { return outputData; } + + public long getUploadDuration() { + return uploadDuration; + } + + public void setUploadDuration(long uploadDuration) { + this.uploadDuration = uploadDuration; + } + + public long getStartUploadTime() { + return startUploadTime; + } + + public void setStartUploadTime(long startUploadTime) { + this.startUploadTime = startUploadTime; + } + + public long getFinishUploadTime() { + return finishUploadTime; + } + + public void setFinishUploadTime(long finishUploadTime) { + this.finishUploadTime = finishUploadTime; + } } diff --git a/src/android/UploadEventDao.java b/src/android/UploadEventDao.java index fade69d9..f7023939 100644 --- a/src/android/UploadEventDao.java +++ b/src/android/UploadEventDao.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; @@ -32,4 +33,7 @@ default void delete(final String id) { delete(ack); } } + + @Update + void update(UploadEvent uploadEvent); } diff --git a/src/android/UploadTask.java b/src/android/UploadTask.java index 919f6e9f..3bda10b8 100644 --- a/src/android/UploadTask.java +++ b/src/android/UploadTask.java @@ -79,6 +79,10 @@ public final class UploadTask extends Worker { public static final String KEY_OUTPUT_STATUS_CODE = "output_status_code"; public static final String KEY_OUTPUT_FAILURE_REASON = "output_failure_reason"; public static final String KEY_OUTPUT_FAILURE_CANCELED = "output_failure_canceled"; + public static final String KEY_OUTPUT_UPLOAD_DURATION = "output_upload_duration"; + public static final String KEY_OUTPUT_UPLOAD_START_TIME = "output_upload_start_time"; + public static final String KEY_OUTPUT_UPLOAD_END_TIME = "output_upload_end_time"; + // private static UploadNotification uploadNotification = null; @@ -96,6 +100,9 @@ public void release() { } private static int concurrency = 1; private static Semaphore concurrentUploads = new Semaphore(concurrency, true); private static Mutex concurrencyLock = new Mutex(); + private long startUploadTime; + private long finishUploadTime; + private long uploadDuration; public UploadTask(@NonNull Context context, @NonNull WorkerParameters workerParams) { @@ -189,6 +196,8 @@ public Result doWork() { return Result.retry(); } + startUploadTime = System.currentTimeMillis(); + // Register me uploadForegroundNotification.progress(getId(), 0f); handleNotification(); @@ -246,6 +255,8 @@ public Result doWork() { return Result.retry(); } } finally { + finishUploadTime = System.currentTimeMillis(); + uploadDuration = finishUploadTime - startUploadTime; // Always remove ourselves from the notification uploadForegroundNotification.done(getId()); } @@ -254,7 +265,10 @@ public Result doWork() { final Data.Builder outputData = new Data.Builder() .putString(KEY_OUTPUT_ID, id) .putBoolean(KEY_OUTPUT_IS_ERROR, false) - .putInt(KEY_OUTPUT_STATUS_CODE, (!DEBUG_SKIP_UPLOAD) ? response.code() : 200); + .putInt(KEY_OUTPUT_STATUS_CODE, (!DEBUG_SKIP_UPLOAD) ? response.code() : 200) + .putLong(KEY_OUTPUT_UPLOAD_DURATION, uploadDuration) + .putLong(KEY_OUTPUT_UPLOAD_START_TIME, startUploadTime) + .putLong(KEY_OUTPUT_UPLOAD_END_TIME, finishUploadTime); // Try read the response body, if any try { diff --git a/src/ios/FileUploader.h b/src/ios/FileUploader.h index aa17e3b0..139e321c 100644 --- a/src/ios/FileUploader.h +++ b/src/ios/FileUploader.h @@ -1,6 +1,7 @@ #import #import "UploadEvent.h" #import +#import NS_ASSUME_NONNULL_BEGIN @protocol FileUploaderDelegate @optional diff --git a/src/ios/FileUploader.m b/src/ios/FileUploader.m index 6471a3c4..76bc9f85 100644 --- a/src/ios/FileUploader.m +++ b/src/ios/FileUploader.m @@ -1,6 +1,7 @@ #import "FileUploader.h" @interface FileUploader() -@property (nonatomic, strong) NSMutableDictionary* responsesData; +@property (nonatomic, strong) NSMutableDictionary *uploadStartTimes; +@property (nonatomic, strong) NSMutableDictionary *responsesData; @property (nonatomic, strong) AFURLSessionManager *manager; @end @@ -20,6 +21,7 @@ -(id)init{ return nil; [UploadEvent setupStorage]; self.responsesData = [[NSMutableDictionary alloc] init]; + self.uploadStartTimes = [[NSMutableDictionary alloc] init]; NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSBundle mainBundle] bundleIdentifier]]; configuration.HTTPMaximumConnectionsPerHost = FileUploader.parallelUploadsLimit; configuration.sessionSendsLaunchEvents = NO; @@ -27,25 +29,40 @@ -(id)init{ __weak FileUploader *weakSelf = self; [self.manager setTaskDidCompleteBlock:^(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSError * _Nullable error) { NSString* uploadId = [NSURLProtocol propertyForKey:kUploadUUIDStrPropertyKey inRequest:task.originalRequest]; + NSDate *startTime = weakSelf.uploadStartTimes[uploadId]; + NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startTime]; NSLog(@"[BackgroundUpload] Task %@ completed with error %@", uploadId, error); if (!error){ NSData* serverData = weakSelf.responsesData[@(task.taskIdentifier)]; NSString* serverResponse = serverData ? [[NSString alloc] initWithData:serverData encoding:NSUTF8StringEncoding] : @""; [weakSelf.responsesData removeObjectForKey:@(task.taskIdentifier)]; - [weakSelf saveAndSendEvent:@{ - @"id" : uploadId, - @"state" : @"UPLOADED", - @"statusCode" : @(((NSHTTPURLResponse *)task.response).statusCode), - @"serverResponse" : serverResponse - }]; + if (isnumber(duration)) { + [weakSelf saveAndSendEvent:@{ + @"id" : uploadId, + @"state" : @"UPLOADED", + @"statusCode" : @(((NSHTTPURLResponse *)task.response).statusCode), + @"serverResponse" : serverResponse, + @"uploadDuration" : @(duration) + }]; + } else { + [weakSelf saveAndSendEvent:@{ + @"id" : uploadId, + @"state" : @"UPLOADED", + @"statusCode" : @(((NSHTTPURLResponse *)task.response).statusCode), + @"serverResponse" : serverResponse, + @"uploadDuration" : @"N/A" + }]; + } } else { + [weakSelf.responsesData removeObjectForKey:@(task.taskIdentifier)]; [weakSelf saveAndSendEvent:@{ @"id" : uploadId, @"state" : @"FAILED", @"error" : error.localizedDescription, - @"errorCode" : @(error.code) + @"errorCode" : @(error.code), }]; } + [weakSelf.uploadStartTimes removeObjectForKey:uploadId]; // Clean up }]; [self.manager setDataTaskDidReceiveDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDataTask * _Nonnull dataTask, NSData * _Nonnull data) { @@ -60,7 +77,7 @@ -(id)init{ } -(void)saveAndSendEvent:(NSDictionary*)data{ - UploadEvent*event = [UploadEvent create:data]; + UploadEvent* event = [UploadEvent create:data]; [self sendEvent:[event dataRepresentation]]; } @@ -89,6 +106,9 @@ -(void)addUpload:(NSDictionary *)payload completionHandler:(void (^)(NSError* er completionHandler:^(NSError *error, NSMutableURLRequest *request) { if (error) return handler(error); + + weakSelf.uploadStartTimes[payload[@"id"]] = [NSDate date]; + __block double lastProgressTimeStamp = 0; [[weakSelf.manager uploadTaskWithRequest:request