diff --git a/README.md b/README.md index 92382efdc..b1f2e8da5 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,9 @@ _**void generateKey(String keyAlias)**_ For key generation using a Key Alias par - NB: \* This class depends on `AndroidLegacyCryptography` class and the `AndroidMCryptography` class which both implement the above in different ways depending on the SDK version. `AndroidLegacyCryptography` has method implementation that are used when the SDK version is less than API level 23 + +The sample app has examples of how these methods have been implemented. The code for it can be found in +the [MainActivity](https://github.com/opensrp/opensrp-client-core/blob/master/sample/src/main/java/org/smartregister/sample/MainActivity.java) class. ## 2. Data management diff --git a/opensrp-app/jacoco.exec b/opensrp-app/jacoco.exec new file mode 100644 index 000000000..d6e562c13 Binary files /dev/null and b/opensrp-app/jacoco.exec differ diff --git a/opensrp-core/build.gradle b/opensrp-core/build.gradle index 449da8e07..c3f7f8a6b 100644 --- a/opensrp-core/build.gradle +++ b/opensrp-core/build.gradle @@ -235,6 +235,10 @@ dependencies { compileOnly platform('com.google.firebase:firebase-bom:30.0.2') compileOnly 'com.google.firebase:firebase-crashlytics' compileOnly 'com.google.firebase:firebase-perf' + + def work_version = "2.7.1" + implementation "androidx.work:work-runtime:$work_version" + // Add the dependency for the Performance Monitoring library //Mockito diff --git a/opensrp-core/src/main/java/org/smartregister/AllConstants.java b/opensrp-core/src/main/java/org/smartregister/AllConstants.java index b44ec988b..a29fa8b9a 100644 --- a/opensrp-core/src/main/java/org/smartregister/AllConstants.java +++ b/opensrp-core/src/main/java/org/smartregister/AllConstants.java @@ -149,6 +149,7 @@ public class AllConstants { public static final String GPS = "gps"; + public static final String IDENTIFIERS = "identifiers"; public interface FORCED_LOGOUT { String MIN_ALLOWED_APP_VERSION_SETTING = "min_allowed_app_version_setting"; @@ -591,4 +592,14 @@ public class KEY { public static final String EVENTS = "events"; public static final String CLIENTS = "clients"; } + + 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 62bac2904..0739b1a2b 100755 --- a/opensrp-core/src/main/java/org/smartregister/Context.java +++ b/opensrp-core/src/main/java/org/smartregister/Context.java @@ -53,6 +53,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; @@ -230,6 +231,7 @@ public class Context { private ManifestRepository manifestRepository; private ClientFormRepository clientFormRepository; private ClientRelationshipRepository clientRelationshipRepository; + private ZeirIdCleanupRepository zeirIdCleanupRepository; ///////////////////////////////////////////////// @@ -1251,5 +1253,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/cryptography/AndroidMCryptography.java b/opensrp-core/src/main/java/org/smartregister/cryptography/AndroidMCryptography.java index 753cc3e72..c8c951cf4 100644 --- a/opensrp-core/src/main/java/org/smartregister/cryptography/AndroidMCryptography.java +++ b/opensrp-core/src/main/java/org/smartregister/cryptography/AndroidMCryptography.java @@ -73,8 +73,7 @@ public byte[] decrypt(byte[] encrypted, String keyAlias) { Cipher c = Cipher.getInstance(AES_MODE); c.init(Cipher.DECRYPT_MODE, getKey(keyAlias), new GCMParameterSpec(128, INITIALIZATION_VECTOR)); - byte[] decodedBytes = c.doFinal(encrypted); - return decodedBytes; + return c.doFinal(encrypted); } catch (Exception e) { Timber.e(e); 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 f31b4c524..987e387d5 100644 --- a/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java +++ b/opensrp-core/src/main/java/org/smartregister/repository/EventClientRepository.java @@ -6,6 +6,7 @@ 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; @@ -25,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; @@ -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; @@ -2346,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/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/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/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/repository/EventClientRepositoryTest.java b/opensrp-core/src/test/java/org/smartregister/repository/EventClientRepositoryTest.java index 21fc5b7b3..48c25c536 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/view/contract/ECDetailTest.java b/opensrp-core/src/test/java/org/smartregister/view/contract/ECDetailTest.java index 768edfba2..4ac5e6079 100644 --- a/opensrp-core/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,9 +13,9 @@ 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); + public void setup(){ + 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 57d2d9dfb..7d062e941 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -79,6 +79,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.1' diff --git a/sample/src/main/java/org/smartregister/sample/MainActivity.java b/sample/src/main/java/org/smartregister/sample/MainActivity.java index 3935af6fe..07f052acc 100644 --- a/sample/src/main/java/org/smartregister/sample/MainActivity.java +++ b/sample/src/main/java/org/smartregister/sample/MainActivity.java @@ -1,6 +1,7 @@ package org.smartregister.sample; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.Menu; @@ -9,9 +10,11 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.CompoundButton; import android.widget.DatePicker; import android.widget.Spinner; import android.widget.TextView; +import android.widget.ToggleButton; import androidx.appcompat.widget.Toolbar; @@ -20,6 +23,7 @@ import org.joda.time.DateTime; import org.joda.time.LocalDate; +import org.smartregister.cryptography.CryptographicHelper; import org.smartregister.cursoradapter.SmartRegisterQueryBuilder; import org.smartregister.sample.fragment.ReportFragment; import org.smartregister.util.AppHealthUtils; @@ -27,16 +31,23 @@ import org.smartregister.util.LangUtils; import org.smartregister.view.activity.MultiLanguageActivity; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import timber.log.Timber; + public class MainActivity extends MultiLanguageActivity { DatePicker picker; Button btnGet; TextView tvw; - + CryptographicHelper cryptographicHelper = null; + TextView encDecTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -49,6 +60,7 @@ protected void onCreate(Bundle savedInstanceState) { Activity activity = this; tvw = (TextView) findViewById(R.id.textView1); picker = (DatePicker) findViewById(R.id.datePicker1); + encDecTextView = (TextView) findViewById(R.id.encrypt_decrypt_tv); picker.setMinDate(new LocalDate().minusYears(2).toDate().getTime()); @@ -140,6 +152,80 @@ public void onNothingSelected(AdapterView parent) { ((TextView) findViewById(R.id.time)).setText(DateUtil.getDuration(new DateTime().minusYears(4).minusMonths(3).minusWeeks(2).minusDays(1))); new AppHealthUtils(findViewById(R.id.show_sync_stats)); + + // File encryption example section + ToggleButton toggle = (ToggleButton) findViewById(R.id.encrypt_decrypt_toggle); + cryptographicHelper = CryptographicHelper.getInstance(this); + String filename = "test.txt"; + String contents = getString(R.string.encrypt_decrypt_string); + // Create a file with the contents above + try (FileOutputStream fos = MainActivity.this.openFileOutput(filename, Context.MODE_PRIVATE)) { + fos.write(contents.getBytes()); + fos.flush(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + + toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + String keyAlias = "sample"; + // Get or create a key with the Alias name sample or create one if id does not exist + if(cryptographicHelper.getKey(keyAlias)==null) + { + cryptographicHelper.generateKey(keyAlias); + Timber.i("key with alias %s generated",keyAlias); + } + + if (isChecked) { + + try { + // read the text.txt while it is in plain text and write to file + FileInputStream inputStream = openFileInput(filename); + byte[] inputBytes = new byte[inputStream.available()]; + inputStream.read(inputBytes); + byte[] encryptedContents = CryptographicHelper.encrypt(inputBytes, keyAlias); + FileOutputStream fileOutputStream = openFileOutput(filename, Context.MODE_PRIVATE); + Timber.i("encrypted stuff to write %S ",new String(encryptedContents)); + encDecTextView.setText(new String(encryptedContents)); + fileOutputStream.write((encryptedContents)); + fileOutputStream.flush(); + + + } catch (IOException e) { + e.printStackTrace(); + } + + } else { + try { + // + FileInputStream inputStream = openFileInput(filename); + byte[] inputBytes = new byte[inputStream.available()]; + inputStream.read(inputBytes); + Timber.i("before decryption %s", new String(inputBytes)); + + byte[] decryptedStuff = CryptographicHelper.decrypt(inputBytes, keyAlias); + encDecTextView.setText(new String(decryptedStuff)); + Timber.i("decrypted content %s", new String(decryptedStuff)); + + FileOutputStream fileOutputStream = openFileOutput(filename, Context.MODE_PRIVATE); + fileOutputStream.write((decryptedStuff)); + fileOutputStream.flush(); + + + } catch (IOException e) { + e.printStackTrace(); + // Error occurred when opening raw file for reading. + } + + + } + } + }); + + } @Override @@ -166,4 +252,10 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + + @Override + protected void onDestroy() { + super.onDestroy(); + cryptographicHelper = null; + } } 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); } } diff --git a/sample/src/main/res/layout-v17/content_main.xml b/sample/src/main/res/layout-v17/content_main.xml index 0ab437a38..e371d7703 100644 --- a/sample/src/main/res/layout-v17/content_main.xml +++ b/sample/src/main/res/layout-v17/content_main.xml @@ -91,5 +91,29 @@ android:text="@string/sync_stats_label" /> + + + + + + diff --git a/sample/src/main/res/layout/content_main.xml b/sample/src/main/res/layout/content_main.xml index 926b1f215..199b03b6f 100644 --- a/sample/src/main/res/layout/content_main.xml +++ b/sample/src/main/res/layout/content_main.xml @@ -91,5 +91,29 @@ android:text="@string/sync_stats_label" /> + + + + + + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 2982e2aa9..6c38a7759 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -7,4 +7,7 @@ Sample Reports Sync Statistics Show Sync Stats (Long Press for Dialog) + Please encrypt or decrypt me by hitting clicking on the switch below + Encrypt + Decrypt