Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement scheduled plan activation #763

Merged
merged 23 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6fba147
:arrow-up: Bump plan-evaluator to 1.5.1-alpha.1-SNAPSHOT
ekigamba Mar 11, 2021
cf72ae8
Schedule periodic actions after add/update plan
ekigamba Mar 11, 2021
3aab914
Add incomplete PlanPeriodicPlanEvaluationService & PlanPeriodicEvalua…
ekigamba Mar 11, 2021
0f4e3ab
Implement PlanPeriodicEvaluationJob
ekigamba Mar 12, 2021
8282975
Implement action PlanPeriodicEvaluationJob scheduling after deleting …
ekigamba Mar 12, 2021
c197380
Pass plan id to periodic plan/action evaluation to cancel jobs after …
ekigamba Mar 12, 2021
ed77f12
Evaluate plan or cancel jobs if plan/action dates are past
ekigamba Mar 12, 2021
bd49583
:arrow-up: Update plan-evaluator to 1.5.1-alpha.2-SNAPSHOT
ekigamba Mar 12, 2021
b355827
Evaluate action in periodic-plan-evaluation-service instead of athe w…
ekigamba Mar 12, 2021
93be001
Fix build issue in PlanIntentServiceHelper
ekigamba Mar 12, 2021
342a72c
Fix NPE in PlanIntentServiceHelper L143
ekigamba Mar 15, 2021
0a0ef7e
Register TypeAdapter for java.sql.Time
ekigamba Mar 15, 2021
41b4534
Fix plan with wrong trigger.timing
ekigamba Mar 15, 2021
def7933
Add static supervisor plan#serverVersion
ekigamba Mar 15, 2021
a85e617
Implement TaskDao#findTasksByJurisdiction(String)
ekigamba Mar 16, 2021
01cc1b2
Update PlanPeriodicEvaluationJob implementation to enable multiple jo…
ekigamba Mar 18, 2021
9290f0d
Update static plan
ekigamba Mar 18, 2021
30612b4
Update static supervisor plan
ekigamba Mar 18, 2021
002e39f
Merge branch 'master' into issue/implement-scheduled-plan-activation
ekigamba Apr 16, 2021
393ec28
Fix codacy issues
ekigamba Apr 16, 2021
554e9d2
Merge branch 'master' into issue/implement-scheduled-plan-activation
ekigamba Jul 15, 2021
fc533b0
Merge branch 'master' into issue/implement-scheduled-plan-activation
ekigamba Sep 28, 2021
def5de3
Add PlanPeriodEvaluationJob & PeriodicTriggerEvaluationHelper tests
ekigamba Sep 28, 2021
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
15 changes: 6 additions & 9 deletions opensrp-app/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true">
<service
android:name=".sync.intent.PlanPeriodicPlanEvaluationService"
android:exported="false"></service>
<service
android:name=".service.ImageUploadSyncService"
android:description="@string/component_desc_image_upload_service"
android:enabled="true"
android:exported="false" />

<service
android:name=".account.AccountService"
android:description="@string/component_desc_account_service"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>

<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
Expand Down Expand Up @@ -106,12 +109,10 @@
android:configChanges="keyboardHidden|orientation"
android:exported="false"
android:screenOrientation="portrait" />

<activity
android:name=".view.activity.NativeHomeActivity"
android:exported="false"
android:screenOrientation="landscape" />

<activity
android:name=".view.activity.SettingsActivity"
android:exported="false"
Expand All @@ -127,15 +128,13 @@
<receiver
android:name=".view.receiver.SyncBroadcastReceiver"
android:exported="false" />

<receiver
android:name=".view.receiver.ConnectivityChangeReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>

<receiver
android:name=".view.receiver.TimeChangedBroadcastReceiver"
android:exported="false">
Expand All @@ -148,7 +147,6 @@
<service
android:name=".service.intentservices.ReplicationIntentService"
android:exported="false" />

<service
android:name=".sync.intent.P2pProcessRecordsService"
android:description="@string/component_desc_p2p_process_records_service"
Expand All @@ -157,10 +155,9 @@
<meta-data
android:name="com.google.android.gms.vision.Dependencies"
android:value="barcode" />

<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />

</application>
</manifest>

</manifest>
4 changes: 4 additions & 0 deletions opensrp-app/src/main/java/org/smartregister/AllConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ public static final class INTENT_KEY {
public static final String TASK_GENERATED = "task_generated";
public static final String DIALOG_TITLE = "dialog_title";
public static final String DIALOG_MESSAGE = "dialog_message";
public static final String PLAN_ID = "plan-id";
public static final String ACTION_CODE = "action-code";
public static final String ACTION_IDENTIFIER = "action-identifier";
public static final String ACTION = "action";
}

public static final class REGISTER_FRAGMENT {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.smartregister.job;

import android.content.Intent;
import android.text.TextUtils;

import androidx.annotation.NonNull;

import com.evernote.android.job.DailyJob;
import com.evernote.android.job.JobRequest;
import com.evernote.android.job.util.support.PersistableBundleCompat;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.smartregister.AllConstants;
import org.smartregister.domain.Action;
import org.smartregister.sync.intent.PlanPeriodicPlanEvaluationService;
import org.smartregister.utils.DateTypeConverter;
import org.smartregister.utils.TaskDateTimeTypeConverter;
import org.smartregister.utils.TimingRepeatTimeTypeConverter;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

import timber.log.Timber;

/**
* PlanPeriodicEvaluationJob runs the periodically-triggered actions. The tags for these jobs are made up of
* the prefix-tag, action-code and action-identifier as {PREFIX_TAG-actionCode-actionIdentifier}. This
* assumes that an action can have only one periodic trigger. In cases where a periodic trigger will have
* multiple timings then this would need to be extended to support such a scenario since only
* a single {@link DailyJob} for a given TAG can exist at any one time.
*
* Use {@link #generateJobTag(String, String)} to generate the appropriate job tag and {@link #isPlanPeriodEvaluationJob(String)}
* to check if a tag belongs to the {@link PlanPeriodicEvaluationJob} in the applications {@link com.evernote.android.job.JobCreator}
*/
public class PlanPeriodicEvaluationJob extends DailyJob {

public static final String PREFIX_TAG = "PlanPeriodicEvaluationJob";
public static final String TAG_FORMAT = "%s-%s-%s";
public static final String SCHEDULE_ADHOC_TAG = "PlanPeriodicEvaluationAdhocJob";

public static Gson gson = new GsonBuilder()
.registerTypeAdapter(DateTime.class, new TaskDateTimeTypeConverter())
.registerTypeAdapter(LocalDate.class, new DateTypeConverter())
.registerTypeAdapter(Time.class, new TimingRepeatTimeTypeConverter())
.create();

public static void scheduleEverydayAt(@NonNull String jobTag, int hour, int minute, @NonNull Action action, String planId) {
JobRequest.Builder jobRequest = new JobRequest.Builder(jobTag);
PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();

persistableBundleCompat.putString(AllConstants.INTENT_KEY.ACTION, getActionJson(action));
persistableBundleCompat.putString(AllConstants.INTENT_KEY.ACTION_CODE, action.getCode());
persistableBundleCompat.putString(AllConstants.INTENT_KEY.ACTION_IDENTIFIER, action.getIdentifier());
persistableBundleCompat.putString(AllConstants.INTENT_KEY.PLAN_ID, planId);
jobRequest.addExtras(persistableBundleCompat);

long startTime = TimeUnit.HOURS.toMillis(hour) + TimeUnit.MINUTES.toMillis(minute);
schedule(jobRequest, startTime, startTime + TimeUnit.MINUTES.toMillis(45));
}

/**
* For jobs that need to be started immediately
*/
public static void scheduleJobImmediately() {
int jobId = startNowOnce(new JobRequest.Builder(SCHEDULE_ADHOC_TAG));
Timber.d("Scheduling job with name " + SCHEDULE_ADHOC_TAG + " immediately with JOB ID " + jobId);
}

@NonNull
@Override
protected DailyJobResult onRunDailyJob(@NonNull Params params) {
Intent intent = new Intent(getContext(), PlanPeriodicPlanEvaluationService.class);
String actionString = params.getExtras().getString(AllConstants.INTENT_KEY.ACTION, null);
String planId = params.getExtras().getString(AllConstants.INTENT_KEY.PLAN_ID, null);
String actionIdentifier = params.getExtras().getString(AllConstants.INTENT_KEY.ACTION_IDENTIFIER, null);
String actionCode = params.getExtras().getString(AllConstants.INTENT_KEY.ACTION_CODE, null);

if (TextUtils.isEmpty(actionString) || TextUtils.isEmpty(planId)) {
return DailyJobResult.CANCEL;
}

intent.putExtra(AllConstants.INTENT_KEY.ACTION, actionString);
intent.putExtra(AllConstants.INTENT_KEY.ACTION_IDENTIFIER, actionIdentifier);
intent.putExtra(AllConstants.INTENT_KEY.ACTION_CODE, actionCode);
intent.putExtra(AllConstants.INTENT_KEY.PLAN_ID, planId);
getContext().startService(intent);

return DailyJobResult.SUCCESS;
}

public static String getActionJson(@NonNull Action action) {
return gson.toJson(action);
}

public static boolean isPlanPeriodEvaluationJob(String tag) {
return (tag.startsWith(PREFIX_TAG) || SCHEDULE_ADHOC_TAG.equals(tag));
}

public static String generateJobTag(String actionIdentifier, String actionCode) {
return String.format(PlanPeriodicEvaluationJob.TAG_FORMAT, PlanPeriodicEvaluationJob.PREFIX_TAG, actionCode, actionIdentifier);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.smartregister.sync.helper;

import androidx.annotation.NonNull;

import com.evernote.android.job.JobManager;

import org.joda.time.DateTime;
import org.smartregister.domain.Action;
import org.smartregister.domain.PlanDefinition;
import org.smartregister.domain.Timing;
import org.smartregister.domain.TimingRepeat;
import org.smartregister.domain.Trigger;
import org.smartregister.job.PlanPeriodicEvaluationJob;
import org.smartregister.pathevaluator.TriggerType;
import org.smartregister.view.activity.DrishtiApplication;

import java.sql.Time;
import java.util.Calendar;
import java.util.List;
import java.util.Set;

import timber.log.Timber;

/**
* Created by Ephraim Kigamba - [email protected] on 11-03-2021.
*/
public class PeriodicTriggerEvaluationHelper {

private DateTime timeNow;

public void reschedulePeriodicPlanEvaluations(List<PlanDefinition> plans) {
for (PlanDefinition plan: plans) {
List<Action> actions = plan.getActions();
if (actions != null && actions.size() > 0) {
for (Action action : actions) {
Set<Trigger> triggers = action.getTrigger();
if (triggers != null) {
for (Trigger trigger: triggers) {
// This assumes that the action has only one periodic trigger
if (triggers != null && trigger.getType() != null
&& TriggerType.PERIODIC.value().equals(trigger.getType())
&& isValidDailyTriggerSchedule(trigger)) {
// Check if the jobs for the action have been scheduled
// Delete & reschedule the job using the action.code as the job ID
int cancelledJobs = cancelJobsForAction(action.getIdentifier(), action.getCode());
Timber.i("Cancelled %d jobs for action-code [%s] and action-identifier [%s]"
, cancelledJobs, action.getCode(), action.getIdentifier());

// Reschedule the job again using the timing
scheduleActionJob(action, trigger.getTimingTiming(), plan.getIdentifier());

break;
}
}
}
}
}

}
}

protected boolean scheduleActionJob(@NonNull Action action, @NonNull Timing timing, @NonNull String planId) {
List<DateTime> eventLists = timing.getEvent();
TimingRepeat timingRepeat = timing.getRepeat();

boolean scheduled = false;

for (DateTime dateTime: eventLists) {
if (dateTime.isBefore(now()) && timingRepeat != null && timingRepeat.getFrequency() == 1
&& timingRepeat.getPeriodUnit().equals(TimingRepeat.DurationCode.d)) {
List<Time> timesOfDay = timingRepeat.getTimeOfDay();

// Schedule a job everyday for each time
for (Time timeOfDay: timesOfDay) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(timeOfDay);

String jobTag = PlanPeriodicEvaluationJob.generateJobTag(action.getIdentifier(), action.getCode());
PlanPeriodicEvaluationJob.scheduleEverydayAt(jobTag, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), action, planId);
scheduled = true;
}
}
}

return scheduled;
}

public boolean isValidDailyTriggerSchedule(Trigger trigger) {
Timing timing = trigger.getTimingTiming();
List<DateTime> eventLists = timing.getEvent();

for (DateTime dateTime: eventLists) {
if (dateTime.isBefore(now())) {
TimingRepeat repeat = timing.getRepeat();
if (repeat != null && repeat.getFrequency() == 1 && repeat.getPeriodUnit().equals(TimingRepeat.DurationCode.d)) {
return true;
}
}
}

return false;
}

public int cancelJobsForAction(String actionIdentifier, String actionCode) {
int jobsCancelled = 0;

String jobTag = PlanPeriodicEvaluationJob.generateJobTag(actionIdentifier, actionCode);

JobManager jobManager = JobManager.create(DrishtiApplication.getInstance());
jobsCancelled = jobManager.cancelAllForTag(jobTag);

return jobsCancelled;
}

protected DateTime now() {
return timeNow != null ? timeNow : DateTime.now();
}

protected void setNow(DateTime timeNow) {
this.timeNow = timeNow;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@
import org.smartregister.util.DateTimeTypeConverter;
import org.smartregister.util.DateTypeConverter;
import org.smartregister.util.Utils;
import org.smartregister.utils.TimingRepeatTimeTypeConverter;

import java.sql.Time;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

Expand All @@ -49,8 +52,10 @@ public class PlanIntentServiceHelper extends BaseHelper {

private final PlanDefinitionRepository planDefinitionRepository;
private final AllSharedPreferences allSharedPreferences;
protected static Gson gson = new GsonBuilder().registerTypeAdapter(DateTime.class, new DateTimeTypeConverter("yyyy-MM-dd"))
protected static Gson gson = new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeConverter("yyyy-MM-dd"))
.registerTypeAdapter(LocalDate.class, new DateTypeConverter())
.registerTypeAdapter(Time.class, new TimingRepeatTimeTypeConverter())
.disableHtmlEscaping()
.create();

Expand All @@ -63,6 +68,8 @@ public class PlanIntentServiceHelper extends BaseHelper {
private SyncProgress syncProgress;

private Trace planSyncTrace;
private ArrayList<PlanDefinition> planIdsToEvaluate = new ArrayList<>();
private PeriodicTriggerEvaluationHelper periodicTriggerEvaluationHelper;

public static PlanIntentServiceHelper getInstance() {
if (instance == null) {
Expand All @@ -76,6 +83,7 @@ private PlanIntentServiceHelper(PlanDefinitionRepository planRepository) {
this.planDefinitionRepository = planRepository;
this.planSyncTrace = initTrace(PLAN_SYNC);
this.allSharedPreferences = CoreLibrary.getInstance().context().allSharedPreferences();
periodicTriggerEvaluationHelper = new PeriodicTriggerEvaluationHelper();
}

public void syncPlans() {
Expand Down Expand Up @@ -117,10 +125,12 @@ private int batchFetchPlansFromServer(boolean returnCount) {
for (PlanDefinition plan : plans) {
try {
planDefinitionRepository.addOrUpdate(plan);
planIdsToEvaluate.add(plan);
} catch (Exception e) {
Timber.e(e, "EXCEPTION %s", e.toString());
}
}

// update most recent server version
if (!Utils.isEmptyCollection(plans)) {
batchFetchCount = plans.size();
Expand All @@ -132,13 +142,18 @@ private int batchFetchPlansFromServer(boolean returnCount) {
// retry fetch since there were items synced from the server
batchFetchPlansFromServer(false);
}

if (!planIdsToEvaluate.isEmpty()) {
periodicTriggerEvaluationHelper.reschedulePeriodicPlanEvaluations(plans);
}
} catch (Exception e) {
Timber.e(e, "EXCEPTION %s", e.toString());
}

return batchFetchCount;
}


private void startPlanTrace(String action) {
String providerId = allSharedPreferences.fetchRegisteredANM();
String team = allSharedPreferences.fetchDefaultTeam(providerId);
Expand Down
Loading