diff --git a/android/appkotlin/src/main/java/io/o2mc/appkotlin/MainActivity.kt b/android/appkotlin/src/main/java/io/o2mc/appkotlin/MainActivity.kt index 44a5a3af..208f02f0 100644 --- a/android/appkotlin/src/main/java/io/o2mc/appkotlin/MainActivity.kt +++ b/android/appkotlin/src/main/java/io/o2mc/appkotlin/MainActivity.kt @@ -12,6 +12,8 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) App.o2mc.track("MainActivityCreated") // access o2mc by property syntax + App.o2mc.setSessionIdentifier() + App.o2mc.forgetByIdentifier(App.o2mc.sessionIdentifier) } /** @@ -32,6 +34,7 @@ class MainActivity : AppCompatActivity() { val text: String = editText.text.toString() // access text by property syntax App.o2mc.setEndpoint(text) + App.o2mc.forgetByIdentifier(App.o2mc.sessionIdentifier) } /** diff --git a/android/appkotlin/src/main/res/layout/activity_main.xml b/android/appkotlin/src/main/res/layout/activity_main.xml index e6393538..62f5f1ca 100644 --- a/android/appkotlin/src/main/res/layout/activity_main.xml +++ b/android/appkotlin/src/main/res/layout/activity_main.xml @@ -157,3 +157,4 @@ + diff --git a/android/sdk/src/main/java/io/o2mc/sdk/O2MC.java b/android/sdk/src/main/java/io/o2mc/sdk/O2MC.java index d4b78194..3c406c12 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/O2MC.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/O2MC.java @@ -136,6 +136,23 @@ public void setSessionIdentifier() { trackingManager.setSessionIdentifier(); } + /** + * Gets the current user's session identifier. + */ + public String getSessionIdentifier() { + return trackingManager.getSessionIdentifier(); + } + + /** + * Removes all (personal) data related to the user with given identifier. + * This is in compliance with the 'Right to be forgotten' GDPR laws. + * + * @param identifier the identifier of the user who will be forgotten + */ + public void forgetByIdentifier(String identifier) { + trackingManager.forget(identifier); + } + /** * Enable or disable logging. * Logging in release builds is disabled. This behavior is immutable. diff --git a/android/sdk/src/main/java/io/o2mc/sdk/TrackingManager.java b/android/sdk/src/main/java/io/o2mc/sdk/TrackingManager.java index c6de4dff..b3f2b879 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/TrackingManager.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/TrackingManager.java @@ -4,8 +4,10 @@ import io.o2mc.sdk.business.DeviceManager; import io.o2mc.sdk.business.batch.BatchManager; import io.o2mc.sdk.business.event.EventManager; +import io.o2mc.sdk.business.operations.OperationManager; import io.o2mc.sdk.domain.DeviceInformation; import io.o2mc.sdk.domain.Event; +import io.o2mc.sdk.domain.Operation; import io.o2mc.sdk.exceptions.O2MCDeviceException; import io.o2mc.sdk.exceptions.O2MCDispatchException; import io.o2mc.sdk.exceptions.O2MCEndpointException; @@ -36,6 +38,7 @@ public class TrackingManager implements O2MCExceptionNotifier { private DeviceManager deviceManager; private EventManager eventManager; + private OperationManager operationManager; private BatchManager batchManager; private DeviceInformation deviceInformation; @@ -48,10 +51,12 @@ public void init(Application application, String endpoint, int dispatchInterval, this.deviceManager = new DeviceManager(); this.eventManager = new EventManager(); + this.operationManager = new OperationManager(); this.batchManager = new BatchManager(); this.deviceManager.init(this, application); this.eventManager.init(this); + this.operationManager.init(this); this.batchManager.init(this, endpoint, dispatchInterval, maxRetries); } @@ -71,6 +76,10 @@ public void track(String eventName) { eventManager.newEvent(eventName); } + public void forget(String identifier) { + operationManager.newOperationWithProperties(Operation.FORGET_BY_ID, identifier); + } + public void trackWithProperties(String eventName, Object value) { eventManager.newEventWithProperties(eventName, value); } @@ -79,10 +88,18 @@ public List getEventsFromBus() { return eventManager.getEvents(); } + public List getOperationsFromBus() { + return operationManager.getOperations(); + } + public void clearEventsFromBus() { eventManager.reset(); } + public void clearOperationsFromBus() { + operationManager.reset(); + } + /** * Return device information. Generate if it hasn't been generated before. * @@ -125,6 +142,10 @@ public void setSessionIdentifier() { batchManager.setIdentifier(Util.generateUUID()); } + public String getSessionIdentifier() { + return batchManager.getIdentifier(); + } + @Override public void notifyException(O2MCException e, boolean isFatal) { if (o2MCExceptionListener != null) { // if listener is set, inform using an exception diff --git a/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchBus.java b/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchBus.java index 455af435..c26767c1 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchBus.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchBus.java @@ -3,6 +3,9 @@ import io.o2mc.sdk.domain.Batch; import io.o2mc.sdk.domain.DeviceInformation; import io.o2mc.sdk.domain.Event; +import io.o2mc.sdk.domain.Operation; +import io.o2mc.sdk.exceptions.O2MCInternalException; +import io.o2mc.sdk.interfaces.O2MCExceptionNotifier; import io.o2mc.sdk.util.TimeUtil; import java.util.ArrayList; import java.util.List; @@ -26,7 +29,10 @@ public class BatchBus { private boolean awaitingCallback; private Batch pendingBatch; - BatchBus() { + private O2MCExceptionNotifier o2mcExceptionNotifier; + + BatchBus(O2MCExceptionNotifier o2mcExceptionNotifier) { + this.o2mcExceptionNotifier = o2mcExceptionNotifier; this.batches = new ArrayList<>(); } @@ -41,11 +47,47 @@ public void setDeviceInformation(DeviceInformation deviceInformation) { } } - public Batch generateBatch(String batchId, List events) { + public Batch generateBatch(String batchId, List events, List operations) { + // Events may be null/empty + // Operations may be null/empty + // But at least one of those lists must be non-empty + + // Make sure we have a valid list of events + List eventsLocal; + if (events == null) { + eventsLocal = new ArrayList<>(); + } else if (events.size() == 0) { + eventsLocal = new ArrayList<>(); + } else { // We have at least one event, use it + eventsLocal = new ArrayList<>(events); + } + + // Make sure we have a valid list of operations + List operationsLocal; + if (operations == null) { + operationsLocal = new ArrayList<>(); + } else if (operations.size() == 0) { + operationsLocal = new ArrayList<>(); + } else { // We have at least one operation, use it + operationsLocal = new ArrayList<>(operations); + } + + if (eventsLocal.size() == 0 && operationsLocal.size() == 0) { + // If we have neither any events nor operations, we shouldn't even be creating this batch + // Something went wrong earlier, notify developer + o2mcExceptionNotifier.notifyException( + new O2MCInternalException( + "There are no events nor operations while generating a batch. We should not be generating a batch without either of those. Find out how we got to this point and prevent it from happening again."), + false + // Not fatal, just unusual behavior which should be looked into before it causes any trouble in the future + ); + } + return new Batch( deviceInformation, TimeUtil.generateTimestamp(), - new ArrayList<>(events), // generate a new list, don't use a reference to the list + eventsLocal, + operationsLocal, batchCounter++, /*add 1 to the counter after this statement*/ batchId, 0 @@ -140,10 +182,15 @@ private Batch mergeBatches(List batches) { allEvents.addAll(b.getEvents()); } + List allOperations = new ArrayList<>(); + for (Batch b : batches) { + allOperations.addAll(b.getOperations()); + } + // The batch ID is the same for every batch in the current user session, doesn't matter if we get the 1st one or the last one String batchId = batches.get(0).getSessionId(); - return generateBatch(batchId, allEvents); + return generateBatch(batchId, allEvents, allOperations); } /** diff --git a/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchManager.java b/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchManager.java index 651c2c47..57779866 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchManager.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/business/batch/BatchManager.java @@ -3,6 +3,7 @@ import io.o2mc.sdk.Config; import io.o2mc.sdk.TrackingManager; import io.o2mc.sdk.domain.Event; +import io.o2mc.sdk.domain.Operation; import io.o2mc.sdk.exceptions.O2MCDispatchException; import io.o2mc.sdk.exceptions.O2MCEndpointException; import io.o2mc.sdk.util.Util; @@ -51,7 +52,7 @@ public class BatchManager extends TimerTask implements Callback { */ public void init(TrackingManager trackingManager, String endpoint, int dispatchInterval, int maxRetries) { - batchBus = new BatchBus(); + batchBus = new BatchBus(trackingManager); batchDispatcher = new BatchDispatcher(this); this.trackingManager = trackingManager; @@ -65,6 +66,10 @@ public void setIdentifier(String identifier) { this.batchId = identifier; } + public String getIdentifier() { + return batchId; + } + /** * Sets the max amount of retries for generating batches. Helps to reduce cpu usage / battery draining. * @@ -149,6 +154,10 @@ private List getEvents() { return trackingManager.getEventsFromBus(); } + private List getOperations() { + return trackingManager.getOperationsFromBus(); + } + /** * Clears all events which are currently in the EventBus. */ @@ -156,6 +165,13 @@ private void clearEvents() { trackingManager.clearEventsFromBus(); } + /** + * Clears all operations which are currently in the OperationBus. + */ + private void clearOperations() { + trackingManager.clearOperationsFromBus(); + } + public void reset() { batchBus.clearBatches(); batchBus.clearPending(); @@ -234,11 +250,12 @@ public void run() { } // Only generate a batch if we have events - if (getEvents().size() > 0) { - batchBus.add(batchBus.generateBatch(batchId, getEvents())); + if (getEvents().size() > 0 || getOperations().size() > 0) { + batchBus.add(batchBus.generateBatch(batchId, getEvents(), getOperations())); LogD(TAG, String.format("run: Newly generated batch contains '%s' events", getEvents().size())); clearEvents(); + clearOperations(); } // If there's a batch pending, skip this run diff --git a/android/sdk/src/main/java/io/o2mc/sdk/business/event/EventManager.java b/android/sdk/src/main/java/io/o2mc/sdk/business/event/EventManager.java index e0e50d64..bb1ad5da 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/business/event/EventManager.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/business/event/EventManager.java @@ -7,15 +7,14 @@ import java.util.List; /** - * Manages everything that's related to events by making use of a EventBus. + * Manages everything that's related to events by making use of an EventBus. */ public class EventManager { private EventBus eventBus; private boolean isStopped; - // Will be used for future exception handling, once this class gets more complex - @SuppressWarnings({ "FieldCanBeLocal", "unused" }) private O2MCExceptionNotifier notifier; + private O2MCExceptionNotifier notifier; public void init(O2MCExceptionNotifier notifier) { this.eventBus = new EventBus(); @@ -59,7 +58,7 @@ public void newEventWithProperties(String eventName, Object value) { if (!Util.isValidEventValue(value)) { notifier.notifyException( - new O2MCTrackException(String.format("Value '%s' is invalid.", value)), + new O2MCTrackException(String.format("Event value '%s' is invalid.", value)), false); // is not fatal for base SDK functionality, next event value may be valid again } diff --git a/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationBus.java b/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationBus.java new file mode 100644 index 00000000..0a1bcd24 --- /dev/null +++ b/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationBus.java @@ -0,0 +1,38 @@ +package io.o2mc.sdk.business.operations; + +import io.o2mc.sdk.domain.Operation; +import io.o2mc.sdk.util.TimeUtil; +import java.util.ArrayList; +import java.util.List; + +/** + * Holds all operations generated by user interaction from the app implementing our SDK. + */ +public class OperationBus { + + private final List operations; + + public OperationBus() { + operations = new ArrayList<>(); + } + + public void add(Operation o) { + operations.add(o); + } + + public void clearOperations() { + operations.clear(); + } + + public List getOperations() { + return operations; + } + + public Operation generateOperation(int operationCode) { + return new Operation(operationCode, null, TimeUtil.generateTimestamp()); + } + + public Operation generateOperationWithProperties(int operationCode, Object properties) { + return new Operation(operationCode, properties, TimeUtil.generateTimestamp()); + } +} diff --git a/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationManager.java b/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationManager.java new file mode 100644 index 00000000..35f26718 --- /dev/null +++ b/android/sdk/src/main/java/io/o2mc/sdk/business/operations/OperationManager.java @@ -0,0 +1,78 @@ +package io.o2mc.sdk.business.operations; + +import io.o2mc.sdk.business.event.EventBus; +import io.o2mc.sdk.domain.Operation; +import io.o2mc.sdk.exceptions.O2MCOperationException; +import io.o2mc.sdk.interfaces.O2MCExceptionNotifier; +import io.o2mc.sdk.util.Util; +import java.util.List; + +/** + * Manages everything that's related to operations by making use of an OperationsBus. + */ +public class OperationManager { + + private OperationBus operationBus; + + // Will be used for future exception handling, once this class gets more complex + @SuppressWarnings({ "FieldCanBeLocal", "unused" }) + private O2MCExceptionNotifier notifier; + + public void init(O2MCExceptionNotifier notifier) { + this.operationBus = new OperationBus(); + this.notifier = notifier; + } + + public List getOperations() { + return operationBus.getOperations(); + } + + /** + * Removes all tracking events which would otherwise be sent upon next dispatch interval. + */ + public void reset() { + operationBus.clearOperations(); + } + + /** + * Generates a new operation and adds it to the OperationBus. + * + * @param operationCode code of operation to generate + */ + public void newOperation(int operationCode) { + if (!Util.isValidOperationCode(operationCode)) { + notifier.notifyException( + new O2MCOperationException(String.format("Operation code '%s' is invalid.", operationCode)), + false); // is not fatal for base SDK functionality, next event name may be valid again + return; + } + + Operation o = operationBus.generateOperation(operationCode); + operationBus.add(o); + } + + /** + * Generates a new operation with additional properties and adds it to the OperationBus. + * + * @param operationCode code of operation to generate + * @param value value to include in the operation + */ + public void newOperationWithProperties(int operationCode, Object value) { + if (!Util.isValidOperationCode(operationCode)) { + notifier.notifyException( + new O2MCOperationException(String.format("Operation code '%s' is invalid.", operationCode)), + false); // is not fatal for base SDK functionality, next event name may be valid again + return; + } + + if (!Util.isValidOperationValue(value)) { + notifier.notifyException( + new O2MCOperationException(String.format("Operation value '%s' is invalid.", operationCode)), + false); // is not fatal for base SDK functionality, next event name may be valid again + return; + } + + Operation o = operationBus.generateOperationWithProperties(operationCode, value); + operationBus.add(o); + } +} diff --git a/android/sdk/src/main/java/io/o2mc/sdk/domain/Batch.kt b/android/sdk/src/main/java/io/o2mc/sdk/domain/Batch.kt index 83bb4fb0..bb88a017 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/domain/Batch.kt +++ b/android/sdk/src/main/java/io/o2mc/sdk/domain/Batch.kt @@ -8,6 +8,7 @@ data class Batch( val device: DeviceInformation?, // may be null, final (provides only a getter automatically) val timestamp: String, // final val events: List, // final + val operations: List, // final val number: Int, // final val sessionId: String?, // may be null, final var retries: Int // not final (additionally provides a setter automatically) diff --git a/android/sdk/src/main/java/io/o2mc/sdk/domain/Operation.kt b/android/sdk/src/main/java/io/o2mc/sdk/domain/Operation.kt new file mode 100644 index 00000000..e92c0948 --- /dev/null +++ b/android/sdk/src/main/java/io/o2mc/sdk/domain/Operation.kt @@ -0,0 +1,16 @@ +package io.o2mc.sdk.domain + +/** + * Holds information about operations for the backend. + */ +data class Operation( + val code: Int, + val value: Any?, // value can be null when sending operations without properties + val timestamp: String +) { + companion object { + const val FORGET_BY_ID = 0 +// const val GET_DATA_BY_ID = 1 // example +// const val MODIFY_BY_ID = 2 // example + } +} diff --git a/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCInternalException.java b/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCInternalException.java new file mode 100644 index 00000000..863d28ba --- /dev/null +++ b/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCInternalException.java @@ -0,0 +1,19 @@ +package io.o2mc.sdk.exceptions; + +/** + * Occurs on internal errors which are intended to be read by O2MC developers. + */ +// Suppress unused because the constructors are likely useful for future development. +@SuppressWarnings("unused") public class O2MCInternalException extends O2MCException { + public O2MCInternalException(String message) { + super(message); + } + + O2MCInternalException(Throwable cause) { + super(cause); + } + + O2MCInternalException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCOperationException.java b/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCOperationException.java new file mode 100644 index 00000000..16b299b9 --- /dev/null +++ b/android/sdk/src/main/java/io/o2mc/sdk/exceptions/O2MCOperationException.java @@ -0,0 +1,19 @@ +package io.o2mc.sdk.exceptions; + +/** + * Occurs on errors related to the tracking methods. + */ +// Suppress unused because the constructors are likely useful for future development. +@SuppressWarnings("unused") public class O2MCOperationException extends O2MCException { + public O2MCOperationException(String message) { + super(message); + } + + public O2MCOperationException(Throwable cause) { + super(cause); + } + + public O2MCOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/sdk/src/main/java/io/o2mc/sdk/util/Util.java b/android/sdk/src/main/java/io/o2mc/sdk/util/Util.java index 32784e12..173084bc 100644 --- a/android/sdk/src/main/java/io/o2mc/sdk/util/Util.java +++ b/android/sdk/src/main/java/io/o2mc/sdk/util/Util.java @@ -148,6 +148,32 @@ public static boolean isValidEventName(String eventName) { return true; } + /** + * Validates correctness of an operation code. + * + * @param operationCode the code to validate + * @return true if operation code is valid + */ + public static boolean isValidOperationCode(int operationCode) { + // TODO: 8/18/18 validate + + // All conditions passed, event name is valid + return true; + } + + /** + * Validates correctness of an operation value. + * + * @param value the value of an operation to validate + * @return true if operation value is valid + */ + public static boolean isValidOperationValue(Object value) { + // TODO: 8/18/18 validate + + // All conditions passed, event value is valid + return true; + } + /** * Validates correctness of an event value. * diff --git a/docs/DATA_SPECIFICATION.md b/docs/DATA_SPECIFICATION.md index e1f18e25..bcab7c0b 100644 --- a/docs/DATA_SPECIFICATION.md +++ b/docs/DATA_SPECIFICATION.md @@ -5,6 +5,7 @@ The data is sent as JSON data. The format contains two main properties, the [`de object { object device; array events { object; }; + array operations { object; }; int number; int retries; string? sessionId; @@ -54,7 +55,7 @@ This object is generated after a [`track()`](API.md#track) [`trackWithProperties ``` object { string name; - object? properties; + object? value; string timestamp; } ``` @@ -63,7 +64,7 @@ object { Name of the triggered event. -#### properties +#### value Optional custom properties object. Properties can be set using the [`trackWithProperties()`](API.md#trackwithproperties) method. @@ -71,6 +72,29 @@ Optional custom properties object. Properties can be set using the [`trackWithPr Event generation time as an ISO 8601 format. +### Operations property +This object is generated when an action on the backend in required. + +``` +object { + int code; + object? value; + string timestamp; +} +``` + +#### code + +Code of the triggered event. + +#### value + +Optional custom properties object. + +#### timestamp + +Operation generation time as an ISO 8601 format. + ### Meta properties These properties contain meta information about the current batch.