diff --git a/gradle.properties b/gradle.properties index 9eae38662..cfb2ff9d7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=5.0.4-SNAPSHOT +VERSION_NAME=5.0.6-SNAPSHOT VERSION_CODE=1 GROUP=org.smartregister POM_SETTING_DESCRIPTION=OpenSRP Client Core Application diff --git a/opensrp-core/build.gradle b/opensrp-core/build.gradle index 4b0f88ead..8c95d3aea 100644 --- a/opensrp-core/build.gradle +++ b/opensrp-core/build.gradle @@ -256,6 +256,9 @@ dependencies { implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'xerces:xercesImpl:2.12.0' + def work_version = "2.7.1" + implementation "androidx.work:work-runtime:$work_version" + // Add the dependency for the Performance Monitoring library implementation 'com.google.firebase:firebase-perf:19.0.7' diff --git a/opensrp-core/src/main/java/org/smartregister/AllConstants.java b/opensrp-core/src/main/java/org/smartregister/AllConstants.java index 61e804c89..2679d9631 100644 --- a/opensrp-core/src/main/java/org/smartregister/AllConstants.java +++ b/opensrp-core/src/main/java/org/smartregister/AllConstants.java @@ -148,6 +148,8 @@ public class AllConstants { public static final String GPS = "gps"; + public static final String IDENTIFIERS = "identifiers"; + public static class Immunizations { public static final String BCG = "bcg"; @@ -586,4 +588,14 @@ public interface SyncInfo { String TASK_UNPROCESSED_EVENTS = "taskUnprocessedEvents"; String NULL_EVENT_SYNC_STATUS = "nullEventSyncStatus"; } + + public static class EventType { + public static final String BITRH_REGISTRATION = "Birth Registration"; + public static final String NEW_WOMAN_REGISTRATION = "New Woman Registration"; + } + + public static class Entity { + public static final String MOTHER = "mother"; + } + } diff --git a/opensrp-core/src/main/java/org/smartregister/Context.java b/opensrp-core/src/main/java/org/smartregister/Context.java index f5b91566a..002c71cd3 100755 --- a/opensrp-core/src/main/java/org/smartregister/Context.java +++ b/opensrp-core/src/main/java/org/smartregister/Context.java @@ -51,6 +51,7 @@ import org.smartregister.repository.TaskRepository; import org.smartregister.repository.TimelineEventRepository; import org.smartregister.repository.UniqueIdRepository; +import org.smartregister.repository.ZeirIdCleanupRepository; import org.smartregister.service.ANMService; import org.smartregister.service.ActionService; import org.smartregister.service.AlertService; @@ -229,6 +230,7 @@ public class Context { private ManifestRepository manifestRepository; private ClientFormRepository clientFormRepository; private ClientRelationshipRepository clientRelationshipRepository; + private ZeirIdCleanupRepository zeirIdCleanupRepository; private static final String SHARED_PREFERENCES_FILENAME = "%s_preferences"; @@ -1252,5 +1254,13 @@ public ClientRelationshipRepository getClientRelationshipRepository() { return clientRelationshipRepository; } + + public ZeirIdCleanupRepository zeirIdCleanupRepository() { + if (zeirIdCleanupRepository == null) { + zeirIdCleanupRepository = new ZeirIdCleanupRepository(); + } + return zeirIdCleanupRepository; + } + /////////////////////////////////////////////////////////////////////////////// } diff --git a/opensrp-core/src/main/java/org/smartregister/cursoradapter/RecyclerViewPaginatedAdapter.java b/opensrp-core/src/main/java/org/smartregister/cursoradapter/RecyclerViewPaginatedAdapter.java index 143a5977e..42052d759 100644 --- a/opensrp-core/src/main/java/org/smartregister/cursoradapter/RecyclerViewPaginatedAdapter.java +++ b/opensrp-core/src/main/java/org/smartregister/cursoradapter/RecyclerViewPaginatedAdapter.java @@ -1,9 +1,13 @@ package org.smartregister.cursoradapter; +import static android.os.Looper.getMainLooper; + import android.database.Cursor; +import android.os.Handler; +import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import android.view.ViewGroup; import org.smartregister.commonregistry.CommonPersonObject; import org.smartregister.commonregistry.CommonPersonObjectClient; @@ -42,7 +46,10 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, Cursor cursor) { if (listItemProvider.isFooterViewHolder(viewHolder)) { - listItemProvider.getFooterView(viewHolder, getCurrentPageCount(), getTotalPageCount(), hasNextPage(), hasPreviousPage()); + // make sure counts are updated before updating the view + (new Handler(getMainLooper())).post(() -> { + listItemProvider.getFooterView(viewHolder, getCurrentPageCount(), getTotalPageCount(), hasNextPage(), hasPreviousPage()); + }); } else { CommonPersonObject personinlist = commonRepository.readAllcommonforCursorAdapter(cursor); CommonPersonObjectClient pClient = new CommonPersonObjectClient(personinlist.getCaseId(), diff --git a/opensrp-core/src/main/java/org/smartregister/domain/DuplicateZeirIdStatus.java b/opensrp-core/src/main/java/org/smartregister/domain/DuplicateZeirIdStatus.java new file mode 100644 index 000000000..9046ede70 --- /dev/null +++ b/opensrp-core/src/main/java/org/smartregister/domain/DuplicateZeirIdStatus.java @@ -0,0 +1,14 @@ +package org.smartregister.domain; + +public enum DuplicateZeirIdStatus { + CLEANED("CLEANED"), PENDING("PENDING"); + private String value; + + DuplicateZeirIdStatus(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/opensrp-core/src/main/java/org/smartregister/job/DuplicateCleanerWorker.java b/opensrp-core/src/main/java/org/smartregister/job/DuplicateCleanerWorker.java new file mode 100644 index 000000000..19676b74e --- /dev/null +++ b/opensrp-core/src/main/java/org/smartregister/job/DuplicateCleanerWorker.java @@ -0,0 +1,33 @@ +package org.smartregister.job; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import org.smartregister.domain.DuplicateZeirIdStatus; +import org.smartregister.util.AppHealthUtils; + +import timber.log.Timber; + +public class DuplicateCleanerWorker extends Worker { + private Context mContext; + + public DuplicateCleanerWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + mContext = context; + } + + @NonNull + @Override + public Result doWork() { + DuplicateZeirIdStatus duplicateZeirIdStatus = AppHealthUtils.cleanUniqueZeirIds(); + Timber.i("Doing some cleaning work"); + if (duplicateZeirIdStatus != null && duplicateZeirIdStatus.equals(DuplicateZeirIdStatus.CLEANED)) + WorkManager.getInstance(mContext).cancelWorkById(this.getId()); + + return Result.success(); + } +} diff --git a/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java b/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java index 095382e03..987e387d5 100644 --- a/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java +++ b/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java @@ -3,7 +3,10 @@ package org.smartregister.repository; +import static org.smartregister.AllConstants.ROWID; + import android.content.ContentValues; +import android.content.Intent; import android.database.Cursor; import android.text.TextUtils; import android.util.Pair; @@ -23,19 +26,24 @@ import org.json.JSONException; import org.json.JSONObject; import org.smartregister.AllConstants; +import org.smartregister.Context; import org.smartregister.CoreLibrary; import org.smartregister.clientandeventmodel.DateUtil; import org.smartregister.domain.Client; import org.smartregister.domain.ClientRelationship; +import org.smartregister.domain.DuplicateZeirIdStatus; import org.smartregister.domain.Event; +import org.smartregister.domain.UniqueId; import org.smartregister.domain.db.Column; import org.smartregister.domain.db.ColumnAttribute; import org.smartregister.domain.db.EventClient; import org.smartregister.p2p.sync.data.JsonData; import org.smartregister.sync.intent.P2pProcessRecordsService; +import org.smartregister.sync.intent.PullUniqueIdsIntentService; import org.smartregister.util.DatabaseMigrationUtils; import org.smartregister.util.JsonFormUtils; import org.smartregister.util.Utils; +import org.smartregister.view.activity.DrishtiApplication; import java.lang.reflect.Type; import java.text.ParseException; @@ -52,8 +60,6 @@ import timber.log.Timber; -import static org.smartregister.AllConstants.ROWID; - /** * Created by keyman on 27/07/2017. */ @@ -65,6 +71,10 @@ public class EventClientRepository extends BaseRepository { public static final String VARCHAR = "VARCHAR"; + public static final String ZEIR_ID = "ZEIR_ID"; + + public static final String M_ZEIR_ID = "M_ZEIR_ID"; + protected Table clientTable; protected Table eventTable; @@ -354,12 +364,13 @@ public Boolean checkIfExistsByFormSubmissionId(Table table, String return false; } - private boolean populateStatement(SQLiteStatement statement, Table table, JSONObject - jsonObject, Map columnOrder) { + private boolean populateStatement(SQLiteStatement statement, Table table, JSONObject jsonObject, Map columnOrder) { if (statement == null) return false; + statement.clearBindings(); List columns; + try { if (table.equals(clientTable)) { columns = Arrays.asList(client_column.values()); @@ -373,10 +384,24 @@ private boolean populateStatement(SQLiteStatement statement, Table table, JSONOb List otherColumns = new ArrayList(columns); if (!otherColumns.isEmpty()) { - otherColumns.removeAll(Arrays.asList(client_column.json, client_column.updatedAt, client_column.syncStatus, client_column.validationStatus, client_column.baseEntityId, - client_column.residence, client_column.locationId, client_column.clientType, - event_column.json, event_column.updatedAt, event_column.syncStatus, event_column.validationStatus, event_column.baseEntityId, event_column.eventId - , event_column.planId, event_column.taskId)); + otherColumns.removeAll(Arrays.asList( + client_column.json, + client_column.updatedAt, + client_column.syncStatus, + client_column.validationStatus, + client_column.baseEntityId, + client_column.residence, + client_column.locationId, + client_column.clientType, + event_column.json, + event_column.updatedAt, + event_column.syncStatus, + event_column.validationStatus, + event_column.baseEntityId, + event_column.eventId, + event_column.planId, + event_column.taskId + )); } for (Column column : otherColumns) { @@ -2142,32 +2167,39 @@ public void markEventAsTaskUnprocessed(String formSubmissionId) { } } + public void markEventsAsSynced(Map syncedEventsClients) { + markEventsAsSynced(syncedEventsClients, null, null); + } + @SuppressWarnings("unchecked") - public void markEventsAsSynced(Map syncedEvents) { + public void markEventsAsSynced(Map syncedEventsClients, Set failedEvents, Set failedClients) { try { - List clients = - syncedEvents.containsKey(AllConstants.KEY.CLIENTS) ? (List) syncedEvents.get( - AllConstants.KEY.CLIENTS) : null; - List events = - syncedEvents.containsKey(AllConstants.KEY.EVENTS) ? (List) syncedEvents.get( - AllConstants.KEY.EVENTS) : null; + List clients = syncedEventsClients.containsKey(AllConstants.KEY.CLIENTS) + ? (List) syncedEventsClients.get(AllConstants.KEY.CLIENTS) + : null; + + List events = syncedEventsClients.containsKey(AllConstants.KEY.EVENTS) + ? (List) syncedEventsClients.get(AllConstants.KEY.EVENTS) + : null; if (clients != null && !clients.isEmpty()) { for (JSONObject client : clients) { String baseEntityId = client.getString(client_column.baseEntityId.name()); - markClientAsSynced(baseEntityId); + if (failedClients == null || !failedClients.contains(baseEntityId)) + markClientAsSynced(baseEntityId); } } + if (events != null && !events.isEmpty()) { for (JSONObject event : events) { String formSubmissionId = event.getString(event_column.formSubmissionId.name()); - markEventAsSynced(formSubmissionId); + if (failedEvents == null || !failedEvents.contains(formSubmissionId)) + markEventAsSynced(formSubmissionId); } } } catch (Exception e) { Timber.e(e); } - } protected List fetchClients(String query, String[] params) { @@ -2324,4 +2356,94 @@ public List getEventsByTaskIds(Set taskIds) { + event_column.taskId.name() + " IN (" + StringUtils.repeat("?", ",", taskIds.size()) + ")", taskIds.toArray(new String[0])); } + + public DuplicateZeirIdStatus cleanDuplicateMotherIds() throws Exception { + String username = Context.getInstance().userService().getAllSharedPreferences().fetchRegisteredANM(); + + UniqueIdRepository uniqueIdRepository = Context.getInstance().getUniqueIdRepository(); + + Map duplicates = Context.getInstance().zeirIdCleanupRepository().getClientsWithDuplicateZeirIds(); + long unusedIdsCount = uniqueIdRepository.countUnUsedIds(); + + Timber.e("%d duplicates for provider: %s - %s", duplicates.size(), username, duplicates.toString()); + + if (duplicates.size() > 0) { + Timber.e( + "%s: %d duplicates for provider: %s - %s\nUnused Unique IDs: %d", + this.getClass().getSimpleName(), + duplicates.size(), + username, + duplicates, + unusedIdsCount + ); + } + + for (Map.Entry duplicate : duplicates.entrySet()) { + String baseEntityId = duplicate.getKey(); + String zeirId = duplicate.getValue(); + + JSONObject clientJson = getClientByBaseEntityId(baseEntityId); + JSONObject identifiers = clientJson.getJSONObject(AllConstants.IDENTIFIERS); + + long unusedIds = uniqueIdRepository.countUnUsedIds(); + if (unusedIds <= 30) { // Mske sure we have enough unused IDs left + Timber.e("%s: No more unique IDs available to assign to %s - %s; provider: %s", this.getClass().getSimpleName(), baseEntityId, zeirId, username); + android.content.Context applicationContext = CoreLibrary.getInstance().context().applicationContext(); + applicationContext.startService(new Intent(applicationContext, PullUniqueIdsIntentService.class)); + } + + UniqueId uniqueId = uniqueIdRepository.getNextUniqueId(); + String newZeirId = uniqueId != null ? uniqueId.getOpenmrsId() : null; + + if (StringUtils.isBlank(newZeirId)) { + Timber.e("No unique ID found to assign to %s; provider: %s", baseEntityId, username); + return DuplicateZeirIdStatus.PENDING; + } + + String eventType = AllConstants.EventType.BITRH_REGISTRATION; + String clientType = clientJson.getString(AllConstants.CLIENT_TYPE); + + if (AllConstants.CHILD_TYPE.equals(clientType)) { + identifiers.put(ZEIR_ID, newZeirId.replaceAll("-", "")); + } else if (AllConstants.Entity.MOTHER.equals(clientType)) { + identifiers.put(M_ZEIR_ID, newZeirId); + eventType = AllConstants.EventType.NEW_WOMAN_REGISTRATION; + } + clientJson.put(AllConstants.IDENTIFIERS, identifiers); + + // Add events to process this + addorUpdateClient(baseEntityId, clientJson); + + // fetch the birth/new woman registration event + List registrationEvent = getEvents( + Collections.singletonList(baseEntityId), + Collections.singletonList(BaseRepository.TYPE_Synced), + Collections.singletonList(eventType) + ); + Event event = null; + if (!registrationEvent.isEmpty()) + event = registrationEvent.get(0).getEvent(); + + Client client = convert(clientJson, Client.class); + + // reprocess the event + DrishtiApplication.getInstance().getClientProcessor().processClient(Collections.singletonList(new EventClient(event, client))); + markClientValidationStatus(baseEntityId, false); + + uniqueIdRepository.close(newZeirId); + + Timber.e("%s: %s - %s updated to %s; provider: %s", this.getClass().getSimpleName(), baseEntityId, zeirId, newZeirId, username); + } + + if (duplicates.size() > 0) { + Timber.d("%s: Successfully processed %d duplicates for provider: %s - %s", + this.getClass().getSimpleName(), + duplicates.size(), + username, + duplicates + ); + } + return DuplicateZeirIdStatus.CLEANED; + } + } diff --git a/opensrp-core/src/main/java/org/smartregister/repository/ZeirIdCleanupRepository.java b/opensrp-core/src/main/java/org/smartregister/repository/ZeirIdCleanupRepository.java new file mode 100644 index 000000000..01418d143 --- /dev/null +++ b/opensrp-core/src/main/java/org/smartregister/repository/ZeirIdCleanupRepository.java @@ -0,0 +1,62 @@ +package org.smartregister.repository; + +import net.sqlcipher.Cursor; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +import timber.log.Timber; + +public class ZeirIdCleanupRepository extends BaseRepository { + + private static final String BASE_ENTITY_ID = "baseEntityId"; + + private static final String DUPLICATES_SQL = + "WITH duplicates AS ( " + + " WITH clients AS ( " + + " SELECT baseEntityId, COALESCE(json_extract(json, '$.identifiers.ZEIR_ID'), json_extract(json, '$.identifiers.M_ZEIR_ID')) zeir_id " + + " FROM client " + + " ) " + + " SELECT b.* FROM (SELECT baseEntityId, zeir_id FROM clients GROUP BY zeir_id HAVING count(zeir_id) > 1) a " + + " INNER JOIN clients b ON a.zeir_id=b.zeir_id " + + " UNION " + + " SELECT * FROM clients WHERE zeir_id IS NULL " + + ") " + + "SELECT baseEntityId, zeir_id, lag(zeir_id) over(order by zeir_id) AS prev_zeir_id FROM duplicates"; + + public Map getClientsWithDuplicateZeirIds() { + Map duplicates = new HashMap<>(); + + Cursor cursor = null; + try { + cursor = getWritableDatabase().rawQuery(DUPLICATES_SQL, new String[]{}); + + while (cursor.moveToNext()) { + String baseEntityId = cursor.getString(cursor.getColumnIndex(BASE_ENTITY_ID)); + String zeirId = cursor.getString(cursor.getColumnIndex("zeir_id")); + + duplicates.put(baseEntityId, zeirId); + + String prevZeirId = null; + try { + prevZeirId = cursor.getString(cursor.getColumnIndex("prev_zeir_id")); + } catch (NullPointerException e) { + Timber.e(e, "null prev_zeir_id"); + } + + if (StringUtils.isNotEmpty(prevZeirId) && (prevZeirId.equals(zeirId))) { + duplicates.put(baseEntityId, prevZeirId); + } + } + } catch (Exception e) { + Timber.e(e); + } finally { + if (cursor != null && !cursor.isClosed()) + cursor.close(); + } + + return duplicates; + } +} diff --git a/opensrp-core/src/main/java/org/smartregister/service/HTTPAgent.java b/opensrp-core/src/main/java/org/smartregister/service/HTTPAgent.java index 441c19173..e5a5f1d0e 100644 --- a/opensrp-core/src/main/java/org/smartregister/service/HTTPAgent.java +++ b/opensrp-core/src/main/java/org/smartregister/service/HTTPAgent.java @@ -60,6 +60,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @@ -680,12 +681,21 @@ public AccountResponse oauth2authenticateCore(StringBuffer requestParamBuffer, S } public AccountResponse oauth2authenticate(String username, char[] password, String grantType, String tokenEndpointURL) { + StringBuffer requestParamBuilder = getRequestParams(username, password); + return oauth2authenticateCore(requestParamBuilder, grantType, tokenEndpointURL); + } + private StringBuffer getRequestParams(String username, char[] password) { StringBuffer requestParamBuilder = new StringBuffer(); requestParamBuilder.append("&username=").append(username); - requestParamBuilder.append("&password=").append(password); - - return oauth2authenticateCore(requestParamBuilder, grantType, tokenEndpointURL); + String urlEncodedPassword = new String(password); + try { + urlEncodedPassword = URLEncoder.encode(urlEncodedPassword, "utf-8"); + } catch (UnsupportedEncodingException e) { + Timber.e(e); + } + requestParamBuilder.append("&password=").append(urlEncodedPassword); + return requestParamBuilder; } public AccountResponse oauth2authenticateRefreshToken(String refreshToken) { diff --git a/opensrp-core/src/main/java/org/smartregister/sync/intent/PullUniqueIdsIntentService.java b/opensrp-core/src/main/java/org/smartregister/sync/intent/PullUniqueIdsIntentService.java index 52edaa86c..06d7b1795 100644 --- a/opensrp-core/src/main/java/org/smartregister/sync/intent/PullUniqueIdsIntentService.java +++ b/opensrp-core/src/main/java/org/smartregister/sync/intent/PullUniqueIdsIntentService.java @@ -5,7 +5,6 @@ */ import android.content.Intent; - import androidx.annotation.VisibleForTesting; import org.json.JSONArray; @@ -27,7 +26,6 @@ public class PullUniqueIdsIntentService extends BaseSyncIntentService { public static final String IDENTIFIERS = "identifiers"; private UniqueIdRepository uniqueIdRepo; - public PullUniqueIdsIntentService() { super("PullUniqueOpenMRSUniqueIdsService"); } @@ -99,5 +97,4 @@ public int onStartCommand(Intent intent, int flags, int startId) { protected HTTPAgent getHttpAgent() { return CoreLibrary.getInstance().context().getHttpAgent(); } - } diff --git a/opensrp-core/src/main/java/org/smartregister/sync/intent/SyncIntentService.java b/opensrp-core/src/main/java/org/smartregister/sync/intent/SyncIntentService.java index 669de9157..fde67dd40 100644 --- a/opensrp-core/src/main/java/org/smartregister/sync/intent/SyncIntentService.java +++ b/opensrp-core/src/main/java/org/smartregister/sync/intent/SyncIntentService.java @@ -1,5 +1,18 @@ package org.smartregister.sync.intent; +import static org.smartregister.AllConstants.COUNT; +import static org.smartregister.AllConstants.PerformanceMonitoring.ACTION; +import static org.smartregister.AllConstants.PerformanceMonitoring.CLIENT_PROCESSING; +import static org.smartregister.AllConstants.PerformanceMonitoring.EVENT_SYNC; +import static org.smartregister.AllConstants.PerformanceMonitoring.FETCH; +import static org.smartregister.AllConstants.PerformanceMonitoring.PUSH; +import static org.smartregister.AllConstants.PerformanceMonitoring.TEAM; +import static org.smartregister.util.PerformanceMonitoringUtils.addAttribute; +import static org.smartregister.util.PerformanceMonitoringUtils.clearTraceAttributes; +import static org.smartregister.util.PerformanceMonitoringUtils.initTrace; +import static org.smartregister.util.PerformanceMonitoringUtils.startTrace; +import static org.smartregister.util.PerformanceMonitoringUtils.stopTrace; + import android.content.Context; import android.content.Intent; import android.util.Pair; @@ -37,29 +50,20 @@ import java.text.MessageFormat; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import timber.log.Timber; -import static org.smartregister.AllConstants.COUNT; -import static org.smartregister.AllConstants.PerformanceMonitoring.ACTION; -import static org.smartregister.AllConstants.PerformanceMonitoring.CLIENT_PROCESSING; -import static org.smartregister.AllConstants.PerformanceMonitoring.EVENT_SYNC; -import static org.smartregister.AllConstants.PerformanceMonitoring.FETCH; -import static org.smartregister.AllConstants.PerformanceMonitoring.PUSH; -import static org.smartregister.AllConstants.PerformanceMonitoring.TEAM; -import static org.smartregister.util.PerformanceMonitoringUtils.addAttribute; -import static org.smartregister.util.PerformanceMonitoringUtils.clearTraceAttributes; -import static org.smartregister.util.PerformanceMonitoringUtils.initTrace; -import static org.smartregister.util.PerformanceMonitoringUtils.startTrace; -import static org.smartregister.util.PerformanceMonitoringUtils.stopTrace; - public class SyncIntentService extends BaseSyncIntentService { public static final String SYNC_URL = "/rest/event/sync"; protected static final int EVENT_PULL_LIMIT = 250; protected static final int EVENT_PUSH_LIMIT = 50; private static final String ADD_URL = "rest/event/add"; + private static final String FAILED_CLIENTS = "failed_clients"; + private static final String FAILED_EVENTS = "failed_events"; private Context context; private HTTPAgent httpAgent; private SyncUtils syncUtils; @@ -72,7 +76,7 @@ public class SyncIntentService extends BaseSyncIntentService { protected ValidateAssignmentHelper validateAssignmentHelper; private long totalRecords; private int fetchedRecords = 0; - private int totalRecordsCount = 0 ; + private int totalRecordsCount = 0; //this variable using to track the sync request goes along with add events/clients private boolean isEmptyToAdd = true; @@ -307,35 +311,35 @@ private boolean pushECToServer(EventClientRepository db) { int totalEventCount = db.getUnSyncedEventsCount(); int eventsUploadedCount = 0; - String baseUrl = CoreLibrary.getInstance().context().configuration().dristhiBaseURL(); if (baseUrl.endsWith(context.getString(R.string.url_separator))) { baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf(context.getString(R.string.url_separator))); } for (int i = 0; i < syncUtils.getNumOfSyncAttempts(); i++) { - Map pendingEvents = db.getUnSyncedEvents(getEventBatchSize()); + Map pendingEventsClients = db.getUnSyncedEvents(getEventBatchSize()); - if (pendingEvents.isEmpty()) { + if (pendingEventsClients.isEmpty()) { break; } // create request body JSONObject request = new JSONObject(); try { - if (pendingEvents.containsKey(AllConstants.KEY.CLIENTS)) { - Object value = pendingEvents.get(AllConstants.KEY.CLIENTS); + if (pendingEventsClients.containsKey(AllConstants.KEY.CLIENTS)) { + Object value = pendingEventsClients.get(AllConstants.KEY.CLIENTS); request.put(AllConstants.KEY.CLIENTS, value); if (value instanceof List) { eventsUploadedCount += ((List) value).size(); } } - if (pendingEvents.containsKey(AllConstants.KEY.EVENTS)) { - request.put(AllConstants.KEY.EVENTS, pendingEvents.get(AllConstants.KEY.EVENTS)); + if (pendingEventsClients.containsKey(AllConstants.KEY.EVENTS)) { + request.put(AllConstants.KEY.EVENTS, pendingEventsClients.get(AllConstants.KEY.EVENTS)); } } catch (JSONException e) { Timber.e(e); } + isEmptyToAdd = false; String jsonPayload = request.toString(); startEventTrace(PUSH, eventsUploadedCount); @@ -344,17 +348,36 @@ private boolean pushECToServer(EventClientRepository db) { baseUrl, ADD_URL), jsonPayload); + if (response.isFailure()) { Timber.e("Events sync failed."); isSuccessfulPushSync = false; } else { - db.markEventsAsSynced(pendingEvents); + // do not mark items in list of failed events/clients as synced + Set failedClients = null; + Set failedEvents = null; + + String responseData = response.payload(); + if (StringUtils.isNotEmpty(responseData)) { + try { + JSONObject failedEventClients = new JSONObject(responseData); + failedClients = getFailed(FAILED_CLIENTS, failedEventClients); + failedEvents = getFailed(FAILED_EVENTS, failedEventClients); + } catch (JSONException e) { + Timber.e(e); + } + } + + db.markEventsAsSynced(pendingEventsClients, failedEvents, failedClients); + Timber.i("Events synced successfully."); + stopTrace(eventSyncTrace); updateProgress(eventsUploadedCount, totalEventCount); - if((totalEventCount - eventsUploadedCount) > 0) + if ((totalEventCount - eventsUploadedCount) > 0) pushECToServer(db); + break; } } @@ -362,6 +385,25 @@ private boolean pushECToServer(EventClientRepository db) { return isSuccessfulPushSync; } + private Set getFailed(String recordType, JSONObject failedEventClients) { + Set set = null; + + try { + JSONArray failed = failedEventClients.getJSONArray(recordType); + + if (failed.length() > 0) { + set = new HashSet<>(); + for (int i = 0; i < failed.length(); i++) { + set.add(failed.getString(i)); + } + } + } catch (JSONException e) { + Timber.e(e); + } + + return set; + } + private void startEventTrace(String action, int count) { SyncConfiguration configs = CoreLibrary.getInstance().getSyncConfiguration(); if (configs.firebasePerformanceMonitoringEnabled()) { @@ -490,8 +532,7 @@ protected String getFormattedBaseUrl() { return baseUrl; } - protected Integer getEventBatchSize(){ + protected Integer getEventBatchSize() { return EVENT_PUSH_LIMIT; } - } diff --git a/opensrp-core/src/main/java/org/smartregister/util/AppHealthUtils.java b/opensrp-core/src/main/java/org/smartregister/util/AppHealthUtils.java index 29e16e026..9e92d305c 100644 --- a/opensrp-core/src/main/java/org/smartregister/util/AppHealthUtils.java +++ b/opensrp-core/src/main/java/org/smartregister/util/AppHealthUtils.java @@ -11,6 +11,7 @@ import android.view.View; import android.widget.ArrayAdapter; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ContextThemeWrapper; @@ -19,6 +20,7 @@ import org.smartregister.AllConstants; import org.smartregister.CoreLibrary; import org.smartregister.R; +import org.smartregister.domain.DuplicateZeirIdStatus; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -26,6 +28,8 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import timber.log.Timber; + /** * Created by ndegwamartin on 26/06/2021. */ @@ -115,6 +119,18 @@ public static void refreshFileSystem(Context context, @VisibleForTesting boolean } } + @Nullable + public static DuplicateZeirIdStatus cleanUniqueZeirIds(){ + try { + return CoreLibrary.getInstance().context().getEventClientRepository() + .cleanDuplicateMotherIds(); + } catch (Exception e) { + Timber.e(e); + } + + return null; + } + public interface HealthStatsView { void performDatabaseDownload(); diff --git a/opensrp-core/src/main/java/org/smartregister/util/Utils.java b/opensrp-core/src/main/java/org/smartregister/util/Utils.java index 050b4a906..43980a361 100644 --- a/opensrp-core/src/main/java/org/smartregister/util/Utils.java +++ b/opensrp-core/src/main/java/org/smartregister/util/Utils.java @@ -992,7 +992,7 @@ public static String getTranslatedLocation(String locationName) { String key = getLocationKeyFromName(locationName); String translatedLocationName = getTranslatedIdentifier(key); - return StringUtils.isNotEmpty(translatedLocationName) ? translatedLocationName : locationName; + return StringUtils.isNotEmpty(translatedLocationName) && !translatedLocationName.equals(key) ? translatedLocationName : locationName; } /** diff --git a/opensrp-core/src/main/java/org/smartregister/view/LocationPickerView.java b/opensrp-core/src/main/java/org/smartregister/view/LocationPickerView.java index 799c16d74..97fdea3cc 100644 --- a/opensrp-core/src/main/java/org/smartregister/view/LocationPickerView.java +++ b/opensrp-core/src/main/java/org/smartregister/view/LocationPickerView.java @@ -78,9 +78,7 @@ public void init() { Collections.sort(sortedLocations); sortedLocations.add(0, defaultLocation); - List translatedLocations = sortedLocations.stream().map(e -> { - return Utils.getTranslatedLocation(e); - }).collect(Collectors.toList()); + List translatedLocations = sortedLocations.stream().map(Utils::getTranslatedLocation).collect(Collectors.toList()); serviceLocationsAdapter = new ServiceLocationsAdapter(context, translatedLocations); locationsLV.setAdapter(serviceLocationsAdapter); diff --git a/opensrp-core/src/main/java/org/smartregister/view/fragment/BaseRegisterFragment.java b/opensrp-core/src/main/java/org/smartregister/view/fragment/BaseRegisterFragment.java index 4433f318e..3b6d64ad8 100644 --- a/opensrp-core/src/main/java/org/smartregister/view/fragment/BaseRegisterFragment.java +++ b/opensrp-core/src/main/java/org/smartregister/view/fragment/BaseRegisterFragment.java @@ -115,7 +115,6 @@ public DialogOption[] serviceModeOptions() { return new DialogOption[]{ }; } - @Override public DialogOption[] sortingOptions() { return new DialogOption[]{ diff --git a/opensrp-core/src/test/java/org/smartregister/dto/ClientFormResponseTest.java b/opensrp-core/src/test/java/org/smartregister/dto/ClientFormResponseTest.java index 76b9f0941..1299564aa 100644 --- a/opensrp-core/src/test/java/org/smartregister/dto/ClientFormResponseTest.java +++ b/opensrp-core/src/test/java/org/smartregister/dto/ClientFormResponseTest.java @@ -41,4 +41,29 @@ public void getClientForm() { Assert.assertEquals(1, clientFormResponse.getClientForm().getId()); Assert.assertEquals("[\"test json\"]", clientFormResponse.getClientForm().getJson()); } + + @Test + public void testSetterAndGetterForClientForm() { + ClientFormResponse clientFormResponse = new ClientFormResponse(null, null); + + ClientFormDTO clientFormDTO = new ClientFormDTO(); + clientFormDTO.setId(1); + clientFormDTO.setJson("[\"test json\"]"); + + ClientFormMetadataDTO clientFormMetadataDTO = new ClientFormMetadataDTO(); + Date now = Calendar.getInstance().getTime(); + clientFormMetadataDTO.setCreatedAt(now); + clientFormMetadataDTO.setId(1L); + clientFormMetadataDTO.setIdentifier("referral/anc_form"); + clientFormMetadataDTO.setJurisdiction("test jurisdiction"); + clientFormMetadataDTO.setLabel("ANC Referral form"); + clientFormMetadataDTO.setModule("ANC"); + clientFormMetadataDTO.setVersion("0.0.1"); + + clientFormResponse.setClientForm(clientFormDTO); + clientFormResponse.setClientFormMetadata(clientFormMetadataDTO); + + Assert.assertEquals(clientFormMetadataDTO, clientFormResponse.getClientFormMetadata()); + Assert.assertEquals(clientFormDTO, clientFormResponse.getClientForm()); + } } \ No newline at end of file diff --git a/opensrp-core/src/test/java/org/smartregister/repository/EventClientRepositoryTest.java b/opensrp-core/src/test/java/org/smartregister/repository/EventClientRepositoryTest.java index 59c6d951d..222fb8875 100644 --- a/opensrp-core/src/test/java/org/smartregister/repository/EventClientRepositoryTest.java +++ b/opensrp-core/src/test/java/org/smartregister/repository/EventClientRepositoryTest.java @@ -1,5 +1,17 @@ package org.smartregister.repository; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.smartregister.AllConstants.ROWID; +import static org.smartregister.repository.BaseRepository.TYPE_InValid; +import static org.smartregister.repository.BaseRepository.TYPE_Task_Unprocessed; +import static org.smartregister.repository.BaseRepository.TYPE_Unsynced; +import static org.smartregister.repository.BaseRepository.TYPE_Valid; + import android.content.ContentValues; import android.util.Pair; @@ -26,6 +38,7 @@ import org.smartregister.clientandeventmodel.DateUtil; import org.smartregister.domain.Event; import org.smartregister.domain.SyncStatus; +import org.smartregister.domain.DuplicateZeirIdStatus; import org.smartregister.domain.db.Column; import org.smartregister.domain.db.ColumnAttribute; import org.smartregister.domain.db.EventClient; @@ -44,15 +57,6 @@ import java.util.Set; import java.util.UUID; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.smartregister.AllConstants.ROWID; -import static org.smartregister.repository.BaseRepository.TYPE_InValid; -import static org.smartregister.repository.BaseRepository.TYPE_Task_Unprocessed; -import static org.smartregister.repository.BaseRepository.TYPE_Unsynced; -import static org.smartregister.repository.BaseRepository.TYPE_Valid; - /** * Created by onaio on 29/08/2017. */ @@ -740,4 +744,72 @@ public MatrixCursor getCursorMaxRowId() { return matrixCursor; } + @Test + public void testCleanDuplicateMotherIdsShouldFixAndMarkDuplicateClientsUnSynced() throws Exception { + String DUPLICATES_SQL = "WITH duplicates AS ( " + + " WITH clients AS ( " + + " SELECT baseEntityId, COALESCE(json_extract(json, '$.identifiers.ZEIR_ID'), json_extract(json, '$.identifiers.M_ZEIR_ID')) zeir_id " + + " FROM client " + + " ) " + + " SELECT b.* FROM (SELECT baseEntityId, zeir_id FROM clients GROUP BY zeir_id HAVING count(zeir_id) > 1) a " + + " INNER JOIN clients b ON a.zeir_id=b.zeir_id " + + " UNION " + + " SELECT * FROM clients WHERE zeir_id IS NULL " + + ") " + + "SELECT baseEntityId, zeir_id, lag(zeir_id) over(order by zeir_id) AS prev_zeir_id FROM duplicates"; + + when(sqliteDatabase.rawQuery(eq(DUPLICATES_SQL), any())).thenReturn(getDuplicateZeirIdsCursor()); + when(sqliteDatabase.rawQuery("SELECT COUNT (*) FROM unique_ids WHERE status=?", new String[]{"not_used"}) ).thenReturn(getUniqueIdCountCursor()); + when(sqliteDatabase.rawQuery("SELECT json FROM client WHERE baseEntityId = ? ", new String[]{"1b6fca83-26d0-46d2-bfba-254de5c4424a"}) ).thenReturn(getClientJsonObjectCursor()); + when(sqliteDatabase.query("unique_ids", new String[]{"_id", "openmrs_id", "status", "used_by", "synced_by", "created_at", "updated_at"}, "status = ?", new String[]{"not_used"}, null, null, "created_at ASC", "1")).thenReturn(getUniqueIdCursor()); + + DuplicateZeirIdStatus duplicateZeirIdStatus = eventClientRepository.cleanDuplicateMotherIds(); + Assert.assertEquals(DuplicateZeirIdStatus.CLEANED, duplicateZeirIdStatus); + verify(sqliteDatabase, times(1)).rawQuery(eq(DUPLICATES_SQL), any()); + verify(sqliteDatabase, times(1)).insert(eq("client"), eq(null), any()); + } + + public MatrixCursor getDuplicateZeirIdsCursor() { + MatrixCursor cursor = new MatrixCursor(new String[]{"baseEntityId", "zeir_id", "prev_zeir_id"}); + cursor.addRow(new Object[]{"1b6fca83-26d0-46d2-bfba-254de5c4424a", "11320561", "11320561"}); + return cursor; + } + + public MatrixCursor getUniqueIdCountCursor() { + MatrixCursor cursor = new MatrixCursor(new String[]{"count(*)"}); + cursor.addRow(new Object[]{"12"}); + return cursor; + } + + public MatrixCursor getUniqueIdCursor() { + MatrixCursor cursor = new MatrixCursor(new String[]{"_id", "openmrs_id", "status", "used_by", "synced_by", "created_at", "updated_at"}); + cursor.addRow(new Object[]{"1", "11432345", null, null, null, null, null}); + return cursor; + } + + public MatrixCursor getClientJsonObjectCursor() { + String clientString = "{\n" + + " \"type\": \"Client\",\n" + + " \"clientType\": \"mother\",\n" + + " \"dateCreated\": \"2019-11-21T15:29:36.799+07:00\",\n" + + " \"baseEntityId\": \"1b6fca83-26d0-46d2-bfba-254de5c4424a\",\n" + + " \"identifiers\": {\n" + + " \"M_ZEIR_ID\": \"1132056-1\"\n" + + " },\n" + + " \"firstName\": \"Test 2\",\n" + + " \"lastName\": \"Duplicate\",\n" + + " \"birthdate\": \"1970-01-01T14:00:00.000+07:00\",\n" + + " \"birthdateApprox\": true,\n" + + " \"deathdateApprox\": false,\n" + + " \"gender\": \"Male\",\n" + + " \"_id\": \"f187d396-c25e-4dd7-adc8-dc921c1f8ae4\",\n" + + " \"_rev\": \"v1\"\n" + + "}"; + + MatrixCursor cursor = new MatrixCursor(new String[]{"json"}); + cursor.addRow(new Object[]{clientString}); + return cursor; + } + + } \ No newline at end of file diff --git a/opensrp-core/src/test/java/org/smartregister/repository/ZeirIdCleanupRepositoryTest.java b/opensrp-core/src/test/java/org/smartregister/repository/ZeirIdCleanupRepositoryTest.java new file mode 100644 index 000000000..0117630be --- /dev/null +++ b/opensrp-core/src/test/java/org/smartregister/repository/ZeirIdCleanupRepositoryTest.java @@ -0,0 +1,73 @@ +package org.smartregister.repository; + +import static android.preference.PreferenceManager.getDefaultSharedPreferences; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; + +import net.sqlcipher.MatrixCursor; +import net.sqlcipher.database.SQLiteDatabase; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.reflect.Whitebox; +import org.robolectric.util.ReflectionHelpers; +import org.smartregister.BaseRobolectricUnitTest; +import org.smartregister.CoreLibrary; +import org.smartregister.repository.ZeirIdCleanupRepository; +import org.smartregister.view.activity.DrishtiApplication; + +import java.util.Map; + +/** + * Created by ndegwamartin on 2019-12-02. + */ +public class ZeirIdCleanupRepositoryTest extends BaseRobolectricUnitTest { + @Mock + private Repository repository; + + @Mock + private SQLiteDatabase sqLiteDatabase; + + private ZeirIdCleanupRepository zeirIdCleanupRepository; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + AllSharedPreferences allSharedPreferences = new AllSharedPreferences(getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())); + ReflectionHelpers.setField(CoreLibrary.getInstance().context(), "allSharedPreferences", allSharedPreferences); + + Whitebox.setInternalState(DrishtiApplication.getInstance(), "repository", repository); + when(repository.getReadableDatabase()).thenReturn(sqLiteDatabase); + when(repository.getWritableDatabase()).thenReturn(sqLiteDatabase); + + zeirIdCleanupRepository = new ZeirIdCleanupRepository(); + } + + @After + public void tearDown() { + Whitebox.setInternalState(DrishtiApplication.getInstance(), "repository", (Repository) null); + } + + @Test + public void getClientsWithDuplicateZeirIdsReturnsReturnsMap() { + when(sqLiteDatabase.rawQuery(anyString(), any())).thenReturn(getZuplicateZeirIdsCursor()); + Map duplicates = zeirIdCleanupRepository.getClientsWithDuplicateZeirIds(); + Assert.assertEquals(2, duplicates.size()); + } + + public MatrixCursor getZuplicateZeirIdsCursor() { + MatrixCursor cursor = new MatrixCursor(new String[]{"baseEntityId", "zeir_id", "prev_zeir_id"}); + cursor.addRow(new Object[]{"1b6fca83-26d0-46d2-bfba-254de5c4424a", "11320561", null}); + cursor.addRow(new Object[]{"951f9ecc-50cf-4af5-ba8f-f2ce18a108b2", "11320561", "11320561"}); + return cursor; + } + +} diff --git a/opensrp-core/src/test/java/org/smartregister/service/HTTPAgentTest.java b/opensrp-core/src/test/java/org/smartregister/service/HTTPAgentTest.java index b019f2bbd..c98a6e48e 100644 --- a/opensrp-core/src/test/java/org/smartregister/service/HTTPAgentTest.java +++ b/opensrp-core/src/test/java/org/smartregister/service/HTTPAgentTest.java @@ -25,6 +25,7 @@ import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.robolectric.util.ReflectionHelpers; import org.smartregister.AllConstants; import org.smartregister.Context; import org.smartregister.CoreLibrary; @@ -1221,4 +1222,14 @@ public void testHttpImagePostConfiguresConnectionRequestCorrectly() throws Excep Mockito.verify(printWriter, Mockito.times(7)).flush(); } + + @Test + public void testOauth2authenticateEncodesPasswordCorrectly() { + HTTPAgent httpAgentSpy = Mockito.spy(httpAgent); + + StringBuffer stringBuffer = ReflectionHelpers.callInstanceMethod(httpAgentSpy, "getRequestParams", + ReflectionHelpers.ClassParameter.from(String.class, "testUser"), + ReflectionHelpers.ClassParameter.from(char[].class, "abc123%^&.".toCharArray())); + Assert.assertEquals("&username=testUser&password=abc123%25%5E%26.", stringBuffer.toString()); + } } diff --git a/opensrp-core/src/test/java/org/smartregister/sync/intent/SyncIntentServiceTest.java b/opensrp-core/src/test/java/org/smartregister/sync/intent/SyncIntentServiceTest.java index 5ac004f4b..ab1b4007f 100644 --- a/opensrp-core/src/test/java/org/smartregister/sync/intent/SyncIntentServiceTest.java +++ b/opensrp-core/src/test/java/org/smartregister/sync/intent/SyncIntentServiceTest.java @@ -1,11 +1,24 @@ package org.smartregister.sync.intent; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.smartregister.sync.intent.SyncIntentService.EVENT_PUSH_LIMIT; + import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import com.google.firebase.perf.metrics.Trace; + +import org.json.JSONArray; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; @@ -35,18 +48,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.smartregister.sync.intent.SyncIntentService.EVENT_PUSH_LIMIT; +import java.util.Set; /** * Created by Richard Kareko on 7/21/20. @@ -393,7 +398,7 @@ public void testSuccessfulPullEcSendsSyncProgressAndSyncStatusBroadcasts() { syncIntentService = spy(syncIntentService); ResponseStatus responseStatus = ResponseStatus.success; Mockito.doReturn(new Response<>(responseStatus, eventSyncPayload).withTotalRecords(2l), - new Response<>(responseStatus, null).withTotalRecords(0l)) + new Response<>(responseStatus, null).withTotalRecords(0l)) .when(httpAgent).postWithJsonResponse(stringArgumentCaptor.capture(), stringArgumentCaptor.capture()); syncIntentService.pullECFromServer(); @@ -439,7 +444,7 @@ public void testPushECToServer() throws Exception { Whitebox.invokeMethod(syncIntentService, "pushECToServer", eventClientRepository); - verify(eventClientRepository).markEventsAsSynced(pendingEvents); + verify(eventClientRepository).markEventsAsSynced(pendingEvents, null, null); verify(syncIntentService).updateProgress(1, 2); String syncUrl = stringArgumentCaptor.getAllValues().get(0); @@ -449,6 +454,59 @@ public void testPushECToServer() throws Exception { } + @Test + public void testPushECToServerVerifyMarkEventsAsSyncedForFailedEventsAndClients() throws Exception { + syncIntentService = spy(syncIntentService); + List clients = new ArrayList<>(); + + JSONObject client = new JSONObject(clientJson); + clients.add(client); + + List events = new ArrayList<>(); + JSONObject event = new JSONObject(eventJson); + events.add(event); + + Map pendingEvents = new HashMap<>(); + pendingEvents.put(AllConstants.KEY.CLIENTS, clients); + pendingEvents.put(AllConstants.KEY.EVENTS, events); + + JSONObject expectedRequest = new JSONObject(); + expectedRequest.put(AllConstants.KEY.CLIENTS, pendingEvents.get(AllConstants.KEY.CLIENTS)); + expectedRequest.put(AllConstants.KEY.EVENTS, pendingEvents.get(AllConstants.KEY.EVENTS)); + + JSONObject failed = new JSONObject(); + failed.put("failed_clients", new JSONArray("[\"1234-5678\"]")); + failed.put("failed_events", new JSONArray("[\"9876-5432\"]")); + + Set failedClients = new HashSet(); + failedClients.add("1234-5678"); + Set failedEvents = new HashSet(); + failedEvents.add("9876-5432"); + + Whitebox.setInternalState(syncIntentService, "httpAgent", httpAgent); + when(eventClientRepository.getUnSyncedEventsCount()).thenReturn(2); + when(eventClientRepository.getUnSyncedEvents(EVENT_PUSH_LIMIT)).thenReturn(pendingEvents, new HashMap<>()); // return empty map on 2nd iteration + Mockito.doReturn(new Response<>(ResponseStatus.success, failed.toString())) + .when(httpAgent).post(stringArgumentCaptor.capture(), stringArgumentCaptor.capture()); + + SyncConfiguration syncConfiguration = Mockito.mock(SyncConfiguration.class); + Trace trace = Mockito.mock(Trace.class); + Mockito.doReturn(1).when(syncConfiguration).getSyncMaxRetries(); + Mockito.doReturn(true).when(syncConfiguration).firebasePerformanceMonitoringEnabled(); + ReflectionHelpers.setField(CoreLibrary.getInstance(), "syncConfiguration", syncConfiguration); + ReflectionHelpers.setField(syncIntentService, "eventSyncTrace", trace); + + Whitebox.invokeMethod(syncIntentService, "pushECToServer", eventClientRepository); + + verify(eventClientRepository).markEventsAsSynced(pendingEvents, failedEvents, failedClients); + verify(syncIntentService).updateProgress(1, 2); + + String syncUrl = stringArgumentCaptor.getAllValues().get(0); + assertEquals("https://sample-stage.smartregister.org/opensrp/rest/event/add", syncUrl); + String requestString = stringArgumentCaptor.getAllValues().get(1); + assertEquals(expectedRequest.toString(), requestString); + } + @Test public void testPullEcFromServerUsingGETUsesCorrectURLAndParams() { diff --git a/opensrp-core/src/test/java/org/smartregister/view/LocationPickerViewTest.java b/opensrp-core/src/test/java/org/smartregister/view/LocationPickerViewTest.java index f20e4ba7a..f3e5e3390 100644 --- a/opensrp-core/src/test/java/org/smartregister/view/LocationPickerViewTest.java +++ b/opensrp-core/src/test/java/org/smartregister/view/LocationPickerViewTest.java @@ -9,6 +9,7 @@ import org.junit.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowDialog; import org.robolectric.util.ReflectionHelpers; @@ -35,6 +36,13 @@ public void setUp() throws Exception { locationPickerView = new LocationPickerView(RuntimeEnvironment.application); } + @Test + public void testConstructors() { + Assert.assertNotNull(new LocationPickerView(RuntimeEnvironment.application)); + Assert.assertNotNull(new LocationPickerView(RuntimeEnvironment.application, Robolectric.buildAttributeSet().build())); + Assert.assertNotNull(new LocationPickerView(RuntimeEnvironment.application, Robolectric.buildAttributeSet().build(), 0)); + } + @Test public void initShouldCorrectlyInitializeLocationPicker() { if (LocationHelper.getInstance() != null) { diff --git a/opensrp-app/src/test/java/org/smartregister/view/contract/ECDetailTest.java b/opensrp-core/src/test/java/org/smartregister/view/contract/ECDetailTest.java similarity index 84% rename from opensrp-app/src/test/java/org/smartregister/view/contract/ECDetailTest.java rename to opensrp-core/src/test/java/org/smartregister/view/contract/ECDetailTest.java index 5574dcd9c..9fb2f3155 100644 --- a/opensrp-app/src/test/java/org/smartregister/view/contract/ECDetailTest.java +++ b/opensrp-core/src/test/java/org/smartregister/view/contract/ECDetailTest.java @@ -1,6 +1,5 @@ package org.smartregister.view.contract; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -14,8 +13,8 @@ public class ECDetailTest { private String caseId = "1234-5678-1234"; @Before public void setup(){ - ecDetail = new ECDetail(caseId,"Kogelo","kisumu", "456", true, - "addres1", "sd-card/photos", new ArrayList(),null, null ); + ecDetail = new ECDetail(caseId,"Kogelo","kisumu", "456", true, + "addres1", "sd-card/photos", new ArrayList(),null, null ); } @Test diff --git a/sample/build.gradle b/sample/build.gradle index e94979783..ffdcc3d12 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -82,6 +82,7 @@ dependencies { transitive=true exclude group: 'com.ibm.fhir', module: 'fhir-model' } + implementation 'androidx.work:work-runtime:2.7.1' jarJar 'com.ibm.fhir:fhir-model:4.7.0' diff --git a/sample/src/main/java/org/smartregister/sample/interactor/LoginInteractor.java b/sample/src/main/java/org/smartregister/sample/interactor/LoginInteractor.java index 3cea406d0..9ddfd5d9f 100644 --- a/sample/src/main/java/org/smartregister/sample/interactor/LoginInteractor.java +++ b/sample/src/main/java/org/smartregister/sample/interactor/LoginInteractor.java @@ -1,8 +1,15 @@ package org.smartregister.sample.interactor; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import org.smartregister.job.DuplicateCleanerWorker; import org.smartregister.login.interactor.BaseLoginInteractor; import org.smartregister.view.contract.BaseLoginContract; +import java.util.concurrent.TimeUnit; + /** * Created by ndegwamartin on 08/05/2020. */ @@ -15,5 +22,8 @@ public LoginInteractor(BaseLoginContract.Presenter loginPresenter) { @Override protected void scheduleJobsPeriodically() { //Schedule your jobs here + WorkRequest cleanZeirIdsWorkRequest = new PeriodicWorkRequest.Builder(DuplicateCleanerWorker.class, 15, TimeUnit.MINUTES) + .build(); + WorkManager.getInstance(this.getApplicationContext()).enqueue(cleanZeirIdsWorkRequest); } }