diff --git a/core/build.gradle b/core/build.gradle index f4f228e8e7..12f6f522ce 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -36,8 +36,8 @@ ext { buildToolsVersion: "28.0.3", minSdkVersion : 19, targetSdkVersion : 28, - versionCode : 210, - versionName : "1.1.0" + versionCode : 211, + versionName : "1.1.1" ] libraries = [ diff --git a/core/gradle.properties b/core/gradle.properties index 0b91150413..0429703b38 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -29,8 +29,8 @@ # Properties which are consumed by plugins/gradle-mvn-push.gradle plugin. # They are used for publishing artifact to snapshot repository. -VERSION_NAME=1.1.0 -VERSION_CODE=210 +VERSION_NAME=1.1.1 +VERSION_CODE=211 GROUP=org.hisp.dhis diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/D2Factory.java b/core/src/androidTest/java/org/hisp/dhis/android/core/D2Factory.java index 29de553439..6098bfc303 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/D2Factory.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/D2Factory.java @@ -40,6 +40,7 @@ import org.hisp.dhis.android.core.arch.storage.internal.InMemoryUnsecureStore; import org.hisp.dhis.android.core.arch.storage.internal.InsecureStore; import org.hisp.dhis.android.core.arch.storage.internal.SecureStore; +import org.hisp.dhis.android.core.maintenance.D2Error; import java.util.Collections; @@ -51,7 +52,7 @@ public static D2 forNewDatabase() { return forNewDatabaseInternal(new InMemorySecureStore(), new InMemoryUnsecureStore()); } - public static D2 forNewDatabaseWithAndroidSecureStore() { + public static D2 forNewDatabaseWithAndroidSecureStore() throws D2Error { Context context = InstrumentationRegistry.getTargetContext().getApplicationContext(); return forNewDatabaseInternal(new AndroidSecureStore(context), new AndroidInsecureStore(context)); } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/storage/internal/CredentialsSecureStorageMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/storage/internal/CredentialsSecureStorageMockIntegrationShould.java index 2e44de9278..a8198c7b28 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/storage/internal/CredentialsSecureStorageMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/storage/internal/CredentialsSecureStorageMockIntegrationShould.java @@ -30,6 +30,7 @@ import androidx.test.InstrumentationRegistry; +import org.hisp.dhis.android.core.maintenance.D2Error; import org.hisp.dhis.android.core.utils.runner.D2JunitRunner; import org.junit.Before; import org.junit.Test; @@ -43,7 +44,7 @@ public class CredentialsSecureStorageMockIntegrationShould { private ObjectKeyValueStore credentialsSecureStore; @Before - public void setUp() { + public void setUp() throws D2Error { credentialsSecureStore = new CredentialsSecureStoreImpl( new AndroidSecureStore(InstrumentationRegistry.getContext().getApplicationContext())); diff --git a/core/src/main/assets/migrations/72.sql b/core/src/main/assets/migrations/72.sql new file mode 100644 index 0000000000..c0a30b585b --- /dev/null +++ b/core/src/main/assets/migrations/72.sql @@ -0,0 +1,2 @@ +# Related to ANDROSDK-1138 +UPDATE D2Error SET errorComponent = 'SDK' WHERE errorComponent IS NULL; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/71.sql b/core/src/main/assets/snapshots/72.sql similarity index 100% rename from core/src/main/assets/snapshots/71.sql rename to core/src/main/assets/snapshots/72.sql diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.java index 12418b206d..05ba344541 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.java @@ -36,7 +36,7 @@ class BaseDatabaseOpenHelper { - static final int VERSION = 71; + static final int VERSION = 72; private final AssetManager assetManager; private final int targetVersion; diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseMigrationExecutor.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseMigrationExecutor.java index fd2fac3006..1dfa8748a7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseMigrationExecutor.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseMigrationExecutor.java @@ -42,7 +42,7 @@ class DatabaseMigrationExecutor { private final DatabaseAdapter databaseAdapter; private final DatabaseMigrationParser parser; - private static final int SNAPSHOT_VERSION = 71; + private static final int SNAPSHOT_VERSION = 72; DatabaseMigrationExecutor(DatabaseAdapter databaseAdapter, AssetManager assetManager) { this.databaseAdapter = databaseAdapter; diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/ParentDatabaseAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/ParentDatabaseAdapter.java index aa33e0dc2d..a65e0315a6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/ParentDatabaseAdapter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/ParentDatabaseAdapter.java @@ -40,7 +40,7 @@ class ParentDatabaseAdapter implements DatabaseAdapter { private DatabaseAdapter getAdapter() { if (adapter == null) { - throw new RuntimeException("Database not yet created. Please login first."); + throw new RuntimeException("Please login to access the database."); } else { return adapter; } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/AndroidSecureStore.java b/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/AndroidSecureStore.java index 651b0a4617..518e8c4d3e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/AndroidSecureStore.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/AndroidSecureStore.java @@ -37,6 +37,10 @@ import androidx.annotation.NonNull; +import org.hisp.dhis.android.core.maintenance.D2Error; +import org.hisp.dhis.android.core.maintenance.D2ErrorCode; +import org.hisp.dhis.android.core.maintenance.D2ErrorComponent; + import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; @@ -55,6 +59,7 @@ import java.security.cert.CertificateException; import java.security.spec.AlgorithmParameterSpec; import java.util.Calendar; +import java.util.Date; import java.util.GregorianCalendar; import javax.crypto.BadPaddingException; @@ -77,7 +82,7 @@ public final class AndroidSecureStore implements SecureStore { private final SharedPreferences preferences; - public AndroidSecureStore(Context context) { + public AndroidSecureStore(Context context) throws D2Error { preferences = context.getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE); KeyStore ks; @@ -95,7 +100,7 @@ public AndroidSecureStore(Context context) { } } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableKeyException ex) { - return; + throw keyStoreError(ex, D2ErrorCode.CANT_ACCESS_KEYSTORE); } // Create a start and end time, for the validity range of the key pair that's about to be @@ -126,7 +131,7 @@ public AndroidSecureStore(Context context) { kpGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) { deleteKeyStoreEntry(ks, ALIAS); - throw new RuntimeException("Couldn't initialize AndroidSecureStore"); + throw keyStoreError(e, D2ErrorCode.CANT_INSTANTIATE_KEYSTORE); } } @@ -134,16 +139,16 @@ public void setData(@NonNull String key, @NonNull String data) { KeyStore ks = null; try { ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE); - ks.load(null); + if (ks.getCertificate(ALIAS) == null) { - return; + throw new RuntimeException("Couldn't find certificate for key: " + key); } PublicKey publicKey = ks.getCertificate(ALIAS).getPublicKey(); if (publicKey == null) { - return; + throw new RuntimeException("Couldn't find publicKey for key: " + key); } String value = encrypt(publicKey, data.getBytes(CHARSET)); @@ -155,7 +160,7 @@ public void setData(@NonNull String key, @NonNull String data) { | IllegalBlockSizeException | BadPaddingException | KeyStoreException | CertificateException | IOException e) { deleteKeyStoreEntry(ks, ALIAS); - throw new RuntimeException("Couldn't store value in AndroidSecureStore for key: " + key); + throw new RuntimeException("Couldn't store value in AndroidSecureStore for key: " + key, e); } } @@ -173,7 +178,7 @@ public String getData(@NonNull String key) { | UnrecoverableEntryException | InvalidKeyException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) { deleteKeyStoreEntry(ks, ALIAS); - throw new RuntimeException("Couldn't get value from AndroidSecureStore for key: " + key); + throw new RuntimeException("Couldn't get value from AndroidSecureStore for key: " + key, e); } } @@ -211,4 +216,14 @@ private void deleteKeyStoreEntry(KeyStore ks, String entry) { Log.w("SECURE_STORE", "Cannot deleted entry " + entry); } } + + private D2Error keyStoreError(Exception ex, D2ErrorCode d2ErrorCode) { + return D2Error.builder() + .errorComponent(D2ErrorComponent.SDK) + .errorCode(d2ErrorCode) + .errorDescription(ex.getMessage()) + .originalException(ex) + .created(new Date()) + .build(); + } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentImportHandler.java b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentImportHandler.java index bc42223a9b..877033aa49 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentImportHandler.java +++ b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentImportHandler.java @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore; import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectStore; import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction; +import org.hisp.dhis.android.core.arch.helpers.internal.EnumHelper; import org.hisp.dhis.android.core.common.DataColumns; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.common.internal.DataStatePropagator; @@ -140,12 +141,14 @@ private void handleEventImportSummaries(EnrollmentImportSummary enrollmentImport } private void handleNoteImportSummary(String enrollmentUid, State state) { + State newNoteState = state.equals(State.SYNCED) ? State.SYNCED : State.TO_POST; String whereClause = new WhereClauseBuilder() - .appendKeyStringValue(DataColumns.STATE, State.TO_POST) + .appendInKeyStringValues( + DataColumns.STATE, EnumHelper.asStringList(State.uploadableStatesIncludingError())) .appendKeyStringValue(NoteTableInfo.Columns.ENROLLMENT, enrollmentUid).build(); List notes = noteStore.selectWhere(whereClause); for (Note note : notes) { - noteStore.update(note.toBuilder().state(state).build()); + noteStore.update(note.toBuilder().state(newNoteState).build()); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.java b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.java index 68038f3ce3..03d86f3cac 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.java @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore; import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectStore; import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction; +import org.hisp.dhis.android.core.arch.helpers.internal.EnumHelper; import org.hisp.dhis.android.core.common.DataColumns; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.enrollment.internal.EnrollmentStore; @@ -167,12 +168,14 @@ private void deleteEventConflicts(String eventUid) { } private void handleNoteImportSummary(String eventUid, State state) { + State newNoteState = state.equals(State.SYNCED) ? State.SYNCED : State.TO_POST; String whereClause = new WhereClauseBuilder() - .appendKeyStringValue(DataColumns.STATE, State.TO_POST) + .appendInKeyStringValues( + DataColumns.STATE, EnumHelper.asStringList(State.uploadableStatesIncludingError())) .appendKeyStringValue(NoteTableInfo.Columns.EVENT, eventUid).build(); List notes = noteStore.selectWhere(whereClause); for (Note note : notes) { - noteStore.update(note.toBuilder().state(state).build()); + noteStore.update(note.toBuilder().state(newNoteState).build()); } } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventPostCall.java b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventPostCall.java index f9f2e3af34..b05f4c0384 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventPostCall.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventPostCall.java @@ -28,14 +28,22 @@ package org.hisp.dhis.android.core.event.internal; +import androidx.annotation.NonNull; + import org.hisp.dhis.android.core.arch.api.executors.internal.APICallExecutor; import org.hisp.dhis.android.core.arch.call.D2Progress; import org.hisp.dhis.android.core.arch.call.internal.D2ProgressManager; +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder; +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; +import org.hisp.dhis.android.core.arch.helpers.internal.EnumHelper; +import org.hisp.dhis.android.core.common.DataColumns; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.event.Event; import org.hisp.dhis.android.core.imports.TrackerImportConflict; import org.hisp.dhis.android.core.imports.internal.EventWebResponse; +import org.hisp.dhis.android.core.note.Note; +import org.hisp.dhis.android.core.note.NoteTableInfo; import org.hisp.dhis.android.core.systeminfo.DHISVersionManager; import org.hisp.dhis.android.core.systeminfo.SystemInfo; import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoModuleDownloader; @@ -49,7 +57,6 @@ import javax.inject.Inject; -import androidx.annotation.NonNull; import dagger.Reusable; import io.reactivex.Observable; @@ -62,6 +69,7 @@ public final class EventPostCall { // adapter and stores private final EventStore eventStore; private final TrackedEntityDataValueStore trackedEntityDataValueStore; + private final IdentifiableObjectStore noteStore; private final EventImportHandler eventImportHandler; private final APICallExecutor apiCallExecutor; @@ -72,6 +80,7 @@ public final class EventPostCall { @NonNull EventService eventService, @NonNull EventStore eventStore, @NonNull TrackedEntityDataValueStore trackedEntityDataValueStore, + @NonNull IdentifiableObjectStore noteStore, @NonNull APICallExecutor apiCallExecutor, @NonNull EventImportHandler eventImportHandler, @NonNull SystemInfoModuleDownloader systemInfoDownloader) { @@ -79,6 +88,7 @@ public final class EventPostCall { this.eventService = eventService; this.eventStore = eventStore; this.trackedEntityDataValueStore = trackedEntityDataValueStore; + this.noteStore = noteStore; this.apiCallExecutor = apiCallExecutor; this.eventImportHandler = eventImportHandler; this.systemInfoDownloader = systemInfoDownloader; @@ -120,18 +130,20 @@ List queryDataToSync(List filteredEvents) { trackedEntityDataValueStore.querySingleEventsTrackedEntityDataValues(); List events = filteredEvents == null ? eventStore.querySingleEventsToPost() : filteredEvents; - List eventRecreated = new ArrayList<>(); + List notes = queryNotesToSync(); + List eventRecreated = new ArrayList<>(); for (Event event : events) { List dataValuesForEvent = dataValueMap.get(event.uid()); + List eventNotes = getEventNotes(notes, event.uid()); + + Event.Builder eventBuilder = event.toBuilder() + .trackedEntityDataValues(dataValuesForEvent) + .notes(eventNotes); if (versionManager.is2_30()) { - eventRecreated.add(event.toBuilder() - .trackedEntityDataValues(dataValuesForEvent) - .geometry(null) - .build()); - } else { - eventRecreated.add(event.toBuilder().trackedEntityDataValues(dataValuesForEvent).build()); + eventBuilder.geometry(null); } + eventRecreated.add(eventBuilder.build()); } markPartitionsAsUploading(eventRecreated); @@ -139,6 +151,25 @@ List queryDataToSync(List filteredEvents) { return eventRecreated; } + private List queryNotesToSync() { + String whereNotesClause = new WhereClauseBuilder() + .appendInKeyStringValues( + DataColumns.STATE, EnumHelper.asStringList(State.uploadableStatesIncludingError())) + .appendKeyStringValue(NoteTableInfo.Columns.NOTE_TYPE, Note.NoteType.EVENT_NOTE) + .build(); + return noteStore.selectWhere(whereNotesClause); + } + + private List getEventNotes(List allNotes, String eventUid) { + List eventNotes = new ArrayList<>(); + for (Note note : allNotes) { + if (eventUid.equals(note.event())) { + eventNotes.add(note); + } + } + return eventNotes; + } + private void handleWebResponse(EventWebResponse webResponse) { if (webResponse == null || webResponse.response() == null) { return; diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java index 981d9360e1..ddcd21b568 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java @@ -37,8 +37,10 @@ public enum D2ErrorCode { APP_NAME_NOT_SET, APP_VERSION_NOT_SET, BAD_CREDENTIALS, + CANT_ACCESS_KEYSTORE, CANT_CREATE_EXISTING_OBJECT, CANT_DELETE_NON_EXISTING_OBJECT, + CANT_INSTANTIATE_KEYSTORE, COULD_NOT_RESERVE_VALUE_ON_SERVER, FILE_NOT_FOUND, FAIL_RESIZING_IMAGE, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstancePostCall.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstancePostCall.java index 9a1428b686..672ed38f45 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstancePostCall.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstancePostCall.java @@ -37,6 +37,7 @@ import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore; import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper; import org.hisp.dhis.android.core.arch.helpers.UidsHelper; +import org.hisp.dhis.android.core.arch.helpers.internal.EnumHelper; import org.hisp.dhis.android.core.common.CoreColumns; import org.hisp.dhis.android.core.common.DataColumns; import org.hisp.dhis.android.core.common.State; @@ -197,7 +198,9 @@ List> getPartitionsToSync(List> attributeValueMap = trackedEntityAttributeValueStore.queryTrackedEntityAttributeValueToPost(); String whereNotesClause = new WhereClauseBuilder() - .appendKeyStringValue(DataColumns.STATE, State.TO_POST).build(); + .appendKeyStringValue( + DataColumns.STATE, EnumHelper.asStringList(State.uploadableStatesIncludingError())) + .build(); List notes = noteStore.selectWhere(whereNotesClause); List targetTrackedEntityInstances; diff --git a/docs/content/developer/compatibility.md b/docs/content/developer/compatibility.md index eb549829b9..b0aa407c24 100644 --- a/docs/content/developer/compatibility.md +++ b/docs/content/developer/compatibility.md @@ -11,3 +11,4 @@ Compatibility table between DHIS2 Android SDK library, DHIS2 core and Android SD | 1.0.2 | 2.29 -> 2.33 | 19 - 28 | | 1.0.3 | 2.29 -> 2.33 | 19 - 28 | | 1.1.0 | 2.29 -> 2.34 | 19 - 28 | +| 1.1.1 | 2.29 -> 2.34 | 19 - 28 | diff --git a/docs/content/developer/database.md b/docs/content/developer/database.md index a4b9b9fa9a..7e2c44a7ab 100644 --- a/docs/content/developer/database.md +++ b/docs/content/developer/database.md @@ -23,4 +23,4 @@ if changed, will encrypt or decrypt the current database without data loss. ### Encryption performance - Database size: the database size is approximately the same, regardless of being encrypted or not. -- Speed: reads and writes are on average 25% slower using an encrypted database. \ No newline at end of file +- Speed: reads and writes are on average 5 to 10% slower using an encrypted database. \ No newline at end of file diff --git a/docs/content/developer/getting-started.md b/docs/content/developer/getting-started.md index 379c66b2dd..78a00535c5 100644 --- a/docs/content/developer/getting-started.md +++ b/docs/content/developer/getting-started.md @@ -10,7 +10,7 @@ Include dependency in build.gradle. ```gradle dependencies { - implementation "org.hisp.dhis:android-core:1.1.0" + implementation "org.hisp.dhis:android-core:1.1.1" ... } ``` diff --git a/docs/dhis2_android_sdk_developer_guide_INDEX.md b/docs/dhis2_android_sdk_developer_guide_INDEX.md index bf6704c27a..792deffbd2 100644 --- a/docs/dhis2_android_sdk_developer_guide_INDEX.md +++ b/docs/dhis2_android_sdk_developer_guide_INDEX.md @@ -3,11 +3,11 @@ title: 'DHIS 2 Android SDK Developer Guide' author: 'DHIS 2' date: year: 2020 -month: April +month: May keywords: [DHIS2, Android] commit: version: master -applicable_txt: 'Applicable to version 1.1.0' +applicable_txt: 'Applicable to version 1.1.1' ---