diff --git a/android-sdk/build.gradle b/android-sdk/build.gradle index e97792c9..32c77298 100644 --- a/android-sdk/build.gradle +++ b/android-sdk/build.gradle @@ -33,7 +33,7 @@ android { } dependencies { - api 'org.sagebionetworks.bridge:rest-client:0.15.6', { + api 'org.sagebionetworks.bridge:rest-client:0.15.23', { exclude group: 'joda-time', module: 'joda-time' } api 'org.sagebionetworks:BridgeDataUploadUtils:0.2.3', { @@ -51,6 +51,12 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + def room_version = "1.1.0" + implementation "android.arch.persistence.room:runtime:$room_version" + annotationProcessor "android.arch.persistence.room:compiler:$room_version" + + implementation 'com.birbit:android-priority-jobqueue:2.0.1' + implementation 'com.google.dagger:dagger-android:2.14.1' implementation 'com.google.dagger:dagger-android-support:2.14.1' annotationProcessor 'com.google.dagger:dagger-android-processor:2.14.1' diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/BridgeJob.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/BridgeJob.java new file mode 100644 index 00000000..3e8e7655 --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/BridgeJob.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.jobqueue; + +import javax.inject.Inject; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.birbit.android.jobqueue.Job; +import com.birbit.android.jobqueue.Params; +import com.birbit.android.jobqueue.RetryConstraint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.sagebionetworks.bridge.android.manager.ActivityManagerV2; +import org.sagebionetworks.bridge.rest.exceptions.BridgeSDKException; + +public abstract class BridgeJob extends Job { + private static final Logger LOG = LoggerFactory.getLogger(BridgeJob.class); + + private static final long BACKOFF_DELAY = 100; + private static final int MAX_TRIES = 5; + + // As per sample code, priority 500 is considered medium priority. + private static final int PRIORITY = 500; + + @Inject + private ActivityManagerV2 activityManagerV2; + + protected BridgeJob() { + super(new Params(PRIORITY).requireNetwork().persist()); + } + + protected ActivityManagerV2 getActivityManagerV2() { + return activityManagerV2; + } + + @Override + protected int getRetryLimit() { + return MAX_TRIES; + } + + @Override + public void onAdded() { + // Job has been scheduled but not executed yet. Nothing to do yet. + } + + @Override + protected void onCancel(int cancelReason, @Nullable Throwable throwable) { + LOG.error("Error running async job, job=" + this.getClass().getName() + ", cancelReason=" + + cancelReason + ", errorClass=" + + (throwable != null ? throwable.getClass().getName() : "null") + ", message=" + + (throwable != null ? throwable.getMessage() : "null")); + } + + @Override + protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, int runCount, + int maxRunCount) { + if (throwable instanceof BridgeSDKException) { + int statusCode = ((BridgeSDKException) throwable).getStatusCode(); + if (statusCode >= 400 && statusCode <= 499) { + // Determinisitic failure from the server. Do not retry. + return RetryConstraint.CANCEL; + } + } + + // In all other cases, we should retry, up to the limit, with exponential backoff. + return RetryConstraint.createExponentialBackoff(runCount, BACKOFF_DELAY); + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/GetActivitiesByDateJob.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/GetActivitiesByDateJob.java new file mode 100644 index 00000000..2d15d834 --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/GetActivitiesByDateJob.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.jobqueue; + +import org.joda.time.DateTime; +import rx.Observable; + +import org.sagebionetworks.bridge.android.persistence.ScheduledActivityEntity; + +public class GetActivitiesByDateJob extends BridgeJob { + private final DateTime startTime; + private final DateTime endTime; + + public GetActivitiesByDateJob(DateTime startTime, DateTime endTime) { + this.startTime = startTime; + this.endTime = endTime; + } + + @Override + public void onRun() { + // Defer to activity manager to get and cache activities. + Observable observable = getActivityManagerV2() + .downloadAndCacheScheduledActivitiesForDates(startTime, endTime); + + // Queued jobs are run asynchronously. The pattern here is that when onRun completes, the + // job is complete, so block on observable completion. + observable.toBlocking().lastOrDefault(null); + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/UpdateActivitiesJob.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/UpdateActivitiesJob.java new file mode 100644 index 00000000..e83b442b --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/jobqueue/UpdateActivitiesJob.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.jobqueue; + +import java.lang.reflect.Type; +import java.util.List; + +import com.google.gson.reflect.TypeToken; +import rx.Completable; +import rx.Observable; + +import org.sagebionetworks.bridge.android.persistence.ScheduledActivityEntity; +import org.sagebionetworks.bridge.rest.RestUtils; +import org.sagebionetworks.bridge.rest.model.ScheduledActivity; + +public class UpdateActivitiesJob extends BridgeJob { + private static final Type ACTIVITY_LIST_TYPE = new TypeToken>(){} + .getType(); + + private transient List activityList; + private final String serializedActivityList; + + public UpdateActivitiesJob(List activityList) { + this.activityList = activityList; + + // Job is serializable so that JobPriorityQueue can persist the job status. However, + // ScheduledActivity is not serializable. To make this serializable, we store it as JSON. + this.serializedActivityList = RestUtils.GSON.toJson(activityList); + } + + private List getActivityList() { + if (activityList == null) { + activityList = RestUtils.GSON.fromJson(serializedActivityList, ACTIVITY_LIST_TYPE); + } + return activityList; + } + + @Override + public void onRun() { + // Defer to activity manager to update activities. + Completable completable = getActivityManagerV2().updateRemoteScheduledActivities( + getActivityList()); + + // Queued jobs are run asynchronously. The pattern here is that when onRun completes, the + // job is complete, so block on completable completion. + completable.await(); + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/manager/ActivityManagerV2.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/manager/ActivityManagerV2.java new file mode 100644 index 00000000..007db91a --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/manager/ActivityManagerV2.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.manager; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.sagebionetworks.bridge.android.util.retrofit.RxUtils.toBodySingle; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import javax.inject.Singleton; + +import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; +import com.birbit.android.jobqueue.JobManager; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Completable; +import rx.Observable; + +import org.sagebionetworks.bridge.android.jobqueue.GetActivitiesByDateJob; +import org.sagebionetworks.bridge.android.persistence.PersistenceUtils; +import org.sagebionetworks.bridge.android.persistence.ScheduledActivityDAO; +import org.sagebionetworks.bridge.android.persistence.ScheduledActivityEntity; +import org.sagebionetworks.bridge.rest.model.ScheduledActivity; + +@AnyThread +@Singleton +public class ActivityManagerV2 { + private static final Logger LOG = LoggerFactory.getLogger(ActivityManagerV2.class); + + // Cache activities for 24 hours. + private static final long CACHE_TTL_MILLIS = 24 * 60 * 60 * 1000; + + @NonNull + private final AtomicReference + authStateHolderAtomicReference; + + @NonNull + private final JobManager jobManager; + + @NonNull + private final ScheduledActivityDAO scheduledActivityDAO; + + public ActivityManagerV2(@NonNull AuthenticationManager authManager, + @NonNull JobManager jobManager, @NonNull ScheduledActivityDAO scheduledActivityDAO) { + checkNotNull(authManager); + checkNotNull(jobManager); + checkNotNull(scheduledActivityDAO); + + this.authStateHolderAtomicReference = authManager.getAuthStateReference(); + this.jobManager = jobManager; + this.scheduledActivityDAO = scheduledActivityDAO; + } + + public Observable getActivities(@NonNull DateTime startTime, + @NonNull DateTime endTime) { + DateTime normalizedEndTime; + try { + normalizedEndTime = normalizeEndTime(startTime, endTime); + } catch (Exception e) { + // in case we do something wrong with JodaTime + return Observable.error(e); + } + + return Observable.fromCallable(() -> scheduledActivityDAO + .getScheduledActivitiesForDates(startTime.getMillis(), normalizedEndTime.getMillis())) + .flatMap(activityList -> { + if (activityList == null || activityList.isEmpty()) { + // Cache has no elements. Call server and return an in-place Observable. + return downloadAndCacheScheduledActivitiesForDates(startTime, + normalizedEndTime); + } else { + // Check cache expiration. + long now = System.currentTimeMillis(); + boolean hasExpired = false; + for (ScheduledActivityEntity oneActivity : activityList) { + if (oneActivity.getLastSyncedOn() + CACHE_TTL_MILLIS < now) { + hasExpired = true; + break; + } + } + + // If we have expired cache elements, kick off a job to update the cache in + // the background. + if (hasExpired) { + jobManager.addJobInBackground(new GetActivitiesByDateJob(startTime, + normalizedEndTime)); + } + + // Return the cached elements we have, in case we don't have network + // connectivity or the call is otherwise too expensive. + return Observable.from(activityList); + } + }).map(PersistenceUtils::databaseActivityToServerActivity); + } + + public Observable downloadAndCacheScheduledActivitiesForDates( + @NonNull DateTime startTime, @NonNull DateTime normalizedEndTime) { + // todo pagination + return toBodySingle(authStateHolderAtomicReference.get().forConsentedUsersApi + .getScheduledActivitiesByDateRange(startTime, normalizedEndTime)) + .toObservable() + .flatMap(list -> Observable.from(list.getItems())) + .map(activity -> { + ScheduledActivityEntity activityEntity = PersistenceUtils + .serverActivityToDatabaseActivity(activity); + scheduledActivityDAO.writeScheduledActivities(activityEntity); + return activityEntity; + }); + } + + private static DateTime normalizeEndTime(@NonNull DateTime startTime, + @NonNull DateTime endTime) { + int startOffset = startTime.getZone().getOffset(startTime); + int endOffset = endTime.getZone().getOffset(endTime); + if (startOffset != endOffset) { + DateTime normalizedEndTime = endTime.toDateTime(DateTimeZone.forOffsetMillis(startOffset)); + LOG.warn("Correcting for mismatched offset. startTime: {}, endTime: {}, newEndTime: {}", startTime, + endTime, normalizedEndTime); + return normalizedEndTime; + } else { + return endTime; + } + } + + public Completable updateActivities(@NonNull List activityList) { + return Completable.fromAction(() -> { + List entityList = Lists.transform(activityList, + PersistenceUtils::serverActivityToDatabaseActivity); + scheduledActivityDAO.writeScheduledActivities(Iterables.toArray(entityList, + ScheduledActivityEntity.class)); + }).mergeWith(Completable.fromAction(() -> { + + })); + } + + public Completable updateRemoteScheduledActivities( + @NonNull List activityList) { + return toBodySingle(authStateHolderAtomicReference.get().forConsentedUsersApi + .updateScheduledActivities(activityList)).toCompletable(); + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/BridgeDatabase.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/BridgeDatabase.java new file mode 100644 index 00000000..04e86b4e --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/BridgeDatabase.java @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.persistence; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.TypeConverters; + +@Database(entities = ScheduledActivityEntity.class, version = 1) +@TypeConverters(Converters.class) +public abstract class BridgeDatabase extends RoomDatabase { + public abstract ScheduledActivityDAO scheduledActivityDAO(); +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/Converters.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/Converters.java new file mode 100644 index 00000000..5d4a4370 --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/Converters.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.persistence; + +import android.arch.persistence.room.TypeConverter; +import org.joda.time.DateTime; +import org.sagebionetworks.bridge.rest.model.ScheduleStatus; + +public class Converters { + @TypeConverter + public static String dateTimeToString(DateTime dateTime) { + return dateTime.toString(); + } + + @TypeConverter + public static DateTime stringToDateTime(String str) { + return DateTime.parse(str); + } + + @TypeConverter + public static String scheduleStatusToString(ScheduleStatus scheduleStatus) { + return scheduleStatus.name(); + } + + @TypeConverter + public static ScheduleStatus stringToScheduleStatus(String str) { + return ScheduleStatus.valueOf(str); + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/PersistenceUtils.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/PersistenceUtils.java new file mode 100644 index 00000000..e8d5e768 --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/PersistenceUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.persistence; + +import java.util.Map; + +import org.sagebionetworks.bridge.rest.RestUtils; +import org.sagebionetworks.bridge.rest.model.Activity; +import org.sagebionetworks.bridge.rest.model.ScheduledActivity; + +public class PersistenceUtils { + public static ScheduledActivity databaseActivityToServerActivity( + ScheduledActivityEntity databaseActivity) { + // Basic attributes. + ScheduledActivity serverActivity = new ScheduledActivity(); + serverActivity.setGuid(databaseActivity.getGuid()); + serverActivity.setExpiresOn(databaseActivity.getExpiresOn()); + serverActivity.setFinishedOn(databaseActivity.getFinishedOn()); + serverActivity.setPersistent(databaseActivity.getPersistent()); + serverActivity.setSchedulePlanGuid(databaseActivity.getSchedulePlanGuid()); + serverActivity.setScheduledOn(databaseActivity.getScheduledOn()); + serverActivity.setStartedOn(databaseActivity.getStartedOn()); + serverActivity.setStatus(databaseActivity.getStatus()); + + // There are some nested attributes and lists that Room doesn't handle very well, so we + // store them as JSON. Convert these back into POJOs. + serverActivity.setActivity(RestUtils.GSON.fromJson(databaseActivity.getActivityJson(), + Activity.class)); + serverActivity.setClientData(RestUtils.GSON.fromJson(databaseActivity.getClientDataJson(), + Map.class)); + + // Server activity has no need for dirty flag or sync time. + + return serverActivity; + } + + public static ScheduledActivityEntity serverActivityToDatabaseActivity( + ScheduledActivity serverActivity) { + // Basic attributes. + ScheduledActivityEntity databaseActivity = new ScheduledActivityEntity(); + databaseActivity.setGuid(serverActivity.getGuid()); + databaseActivity.setExpiresOn(serverActivity.getExpiresOn()); + databaseActivity.setFinishedOn(serverActivity.getFinishedOn()); + databaseActivity.setPersistent(serverActivity.isPersistent()); + databaseActivity.setSchedulePlanGuid(serverActivity.getSchedulePlanGuid()); + databaseActivity.setScheduledOn(serverActivity.getScheduledOn()); + databaseActivity.setStartedOn(serverActivity.getStartedOn()); + databaseActivity.setStatus(serverActivity.getStatus()); + + // There are some nested attributes and lists that Room doesn't handle very well. Convert + // these to JSON. + databaseActivity.setActivityJson(RestUtils.GSON.toJson(serverActivity.getActivity())); + databaseActivity.setClientDataJson(RestUtils.GSON.toJson(serverActivity.getClientData())); + + // Scheduled activities pulled from the server are generally not dirty and we just updated. + databaseActivity.setDirty(false); + databaseActivity.setLastSyncedOn(System.currentTimeMillis()); + + return databaseActivity; + } +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityDAO.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityDAO.java new file mode 100644 index 00000000..e6c876bf --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityDAO.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.persistence; + +import java.util.List; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; + +@Dao +public interface ScheduledActivityDAO { + @Query("SELECT * FROM ScheduledActivityEntity WHERE guid in (:guids)") + List getScheduledActivitiesByGuid(String... guids); + + @Query("SELECT * FROM ScheduledActivityEntity WHERE scheduledOn >= :startTime AND scheduledOn < :endTime") + List getScheduledActivitiesForDates(long startTime, long endTime); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void writeScheduledActivities(ScheduledActivityEntity... scheduledActivities); +} diff --git a/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityEntity.java b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityEntity.java new file mode 100644 index 00000000..1338162e --- /dev/null +++ b/android-sdk/src/main/java/org/sagebionetworks/bridge/android/persistence/ScheduledActivityEntity.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.sagebionetworks.bridge.android.persistence; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; +import org.joda.time.DateTime; +import org.sagebionetworks.bridge.rest.model.ScheduleStatus; + +@Entity +public class ScheduledActivityEntity { + @PrimaryKey + private String guid; + + private String activityJson; + private String clientDataJson; + private boolean dirty; + private DateTime expiresOn; + private DateTime finishedOn; + private long lastSyncedOn; + private Boolean persistent; + private String schedulePlanGuid; + private DateTime scheduledOn; + private DateTime startedOn; + private ScheduleStatus status; + + public String getGuid() { + return guid; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public String getActivityJson() { + return activityJson; + } + + public void setActivityJson(String activityJson) { + this.activityJson = activityJson; + } + + public String getClientDataJson() { + return clientDataJson; + } + + public void setClientDataJson(String clientDataJson) { + this.clientDataJson = clientDataJson; + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean dirty) { + this.dirty = dirty; + } + + public DateTime getExpiresOn() { + return expiresOn; + } + + public void setExpiresOn(DateTime expiresOn) { + this.expiresOn = expiresOn; + } + + public DateTime getFinishedOn() { + return finishedOn; + } + + public void setFinishedOn(DateTime finishedOn) { + this.finishedOn = finishedOn; + } + + public long getLastSyncedOn() { + return lastSyncedOn; + } + + public void setLastSyncedOn(long lastSyncedOn) { + this.lastSyncedOn = lastSyncedOn; + } + + public Boolean getPersistent() { + return persistent; + } + + public void setPersistent(Boolean persistent) { + this.persistent = persistent; + } + + public String getSchedulePlanGuid() { + return schedulePlanGuid; + } + + public void setSchedulePlanGuid(String schedulePlanGuid) { + this.schedulePlanGuid = schedulePlanGuid; + } + + public DateTime getScheduledOn() { + return scheduledOn; + } + + public void setScheduledOn(DateTime scheduledOn) { + this.scheduledOn = scheduledOn; + } + + public DateTime getStartedOn() { + return startedOn; + } + + public void setStartedOn(DateTime startedOn) { + this.startedOn = startedOn; + } + + public ScheduleStatus getStatus() { + return status; + } + + public void setStatus(ScheduleStatus status) { + this.status = status; + } +}