From 196baf6c09491363a9dfa36bc3d68b759733fe74 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 17 Nov 2023 14:49:41 +0300 Subject: [PATCH 1/5] feat: new user module --- CHANGELOG.md | 35 ++ .../main/java/ly/count/sdk/java/Countly.java | 18 +- .../main/java/ly/count/sdk/java/Event.java | 24 +- .../java/ly/count/sdk/java/UserEditor.java | 182 ++++++- .../count/sdk/java/internal/CoreFeature.java | 2 +- .../java/internal/ImmediateRequestMaker.java | 4 +- .../sdk/java/internal/ModuleUserProfile.java | 435 +++++++++++++++++ .../ly/count/sdk/java/internal/SDKCore.java | 11 + .../ly/count/sdk/java/internal/Transport.java | 46 +- .../sdk/java/internal/UserEditorImpl.java | 445 ++++-------------- .../sdk/java/internal/UserEditorTests.java | 3 +- 11 files changed, 809 insertions(+), 396 deletions(-) create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 365ba8838..20e33bcf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## XX.XX.XX + +* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: + * "picture(byte[])" + * "setLocale(String)" + +* Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. + +* The following methods are deprecated from the "UserEditor" interface: + * "commit()" instead use "Countly::userProfile::save" via "instance()" call + * "pushUnique(String, Object)" instead use "Countly::userProfile::pushUnique" via "instance()" call + * "pull(String, Object)" instead use "Countly::userProfile::pull" via "instance()" call + * "push(String, Object)" instead use "Countly::userProfile::push" via "instance()" call + * "setOnce(String, Object)" instead use "Countly::userProfile::setOnce" via "instance()" call + * "max(String, double)" instead use "Countly::userProfile::saveMax" via "instance()" call + * "min(String, double)" instead use "Countly::userProfile::saveMin" via "instance()" call + * "mul(String, double)" instead use "Countly::userProfile::multiply" via "instance()" call + * "inc(String, int)" instead use "Countly::userProfile::incrementBy" via "instance()" call + * "optOutFromLocationServices()" todo add replacement func when location module added + * "setLocation(double, double)" todo add replacement func when location module added + * "setLocation(String)" todo add replacement func when location module added + * "setCountry(String)" todo add replacement func when location module added + * "setCity(String)" todo add replacement func when location module added + * "setGender(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(int)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setBirthyear(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setEmail(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setName(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setUsername(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPhone(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setPicturePath(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + ## 23.10.1 * Fixed a bug where getting the feedback widget list would fail if "salt" was enabled. diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Countly.java b/sdk-java/src/main/java/ly/count/sdk/java/Countly.java index add367999..ee9c63d5b 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Countly.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Countly.java @@ -11,6 +11,7 @@ import ly.count.sdk.java.internal.ModuleEvents; import ly.count.sdk.java.internal.ModuleFeedback; import ly.count.sdk.java.internal.ModuleRemoteConfig; +import ly.count.sdk.java.internal.ModuleUserProfile; import ly.count.sdk.java.internal.SDKCore; /** @@ -281,7 +282,7 @@ public DeviceIdType getDeviceIdType() { return DeviceIdType.fromInt(sdk.config.getDeviceId().strategy, L); } - /** + /** * Change device id with merging * * @param id new user / device id string, cannot be empty @@ -448,6 +449,21 @@ public Event timedEvent(String key) { return ((Session) sdk.session(null)).timedEvent(key); } + /** + * UserProfile interface to use user profile feature. + * + * @return {@link ModuleUserProfile.UserProfile} instance. + */ + public ModuleUserProfile.UserProfile userProfile() { + if (!isInitialized()) { + if (L != null) { + L.e("[Countly] userProfile, SDK is not initialized yet."); + } + return null; + } + return sdk.userProfile(); + } + /** * Get current User Profile object. * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Event.java b/sdk-java/src/main/java/ly/count/sdk/java/Event.java index 90cf054e0..3da6b7060 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Event.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Event.java @@ -1,11 +1,13 @@ package ly.count.sdk.java; -import ly.count.sdk.java.internal.ModuleEvents; -import javax.annotation.Nonnull; import java.util.Map; +import javax.annotation.Nonnull; +import ly.count.sdk.java.internal.ModuleEvents; /** - * Event interface. By default event is created with count=1 and all other fields empty or 0. + * Event interface. By default, event is created with count=1 and all other fields empty or 0. + * + * @deprecated this class is deprecated, use {@link ModuleEvents.Events} instead */ public interface Event { @@ -13,7 +15,7 @@ public interface Event { * Add event to the buffer, send it to the server in case number of events in the session * is equal or bigger than {@link Config#eventQueueThreshold} or wait until next {@link Session#update()}. * - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ void record(); @@ -23,7 +25,7 @@ public interface Event { * send it to the server in case number of events in the session is equal or bigger * than {@link Config#eventQueueThreshold} or wait until next {@link Session#update()}. * - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#endEvent(String, Map, int, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#endEvent(String, Map, int, Double)} instead */ void endAndRecord(); @@ -33,7 +35,7 @@ public interface Event { * @param key key of segment, must not be null or empty * @param value value of segment, must not be null or empty * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event addSegment(@Nonnull String key, @Nonnull String value); @@ -44,7 +46,7 @@ public interface Event { * segmentation from; cannot contain nulls or empty strings; must have * even length * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event addSegments(@Nonnull String... segmentation); @@ -53,7 +55,7 @@ public interface Event { * * @param segmentation map of segment pairs ({key1: value1, key2: value2} * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event setSegmentation(@Nonnull Map segmentation); @@ -62,7 +64,7 @@ public interface Event { * * @param count event count, cannot be 0 * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event setCount(int count); @@ -71,7 +73,7 @@ public interface Event { * * @param sum event sum * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event setSum(double sum); @@ -80,7 +82,7 @@ public interface Event { * * @param duration event duration * @return this instance for method chaining - * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, Map, int, Double, Double)} instead */ Event setDuration(double duration); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/UserEditor.java b/sdk-java/src/main/java/ly/count/sdk/java/UserEditor.java index 6f5101c64..b3da5715e 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/UserEditor.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/UserEditor.java @@ -1,9 +1,12 @@ package ly.count.sdk.java; +import ly.count.sdk.java.internal.ModuleUserProfile; + /** * Editor object for {@link User} modifications. Changes applied only after {@link #commit()} call. + * + * @deprecated All functions of this class are deprecated, please use {@link Countly#userProfile()} instead via "instance()" call */ - public interface UserEditor { /** * Sets property of user profile to the value supplied. All standard Countly properties @@ -15,57 +18,226 @@ public interface UserEditor { * @param value value for this property, null to delete property * @return this instance for method chaining * @see User + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead */ UserEditor set(String key, Object value); + /** + * Sets custom property of user profile to the value supplied. + * + * @param key name of user profile property + * @param value value for this property, null to delete property + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setCustom(String key, Object value); + /** + * Sets name of the user + * + * @param value name of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setName(String value); + /** + * Sets username of the user + * + * @param value username of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setUsername(String value); + /** + * Sets email of the user + * + * @param value email of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setEmail(String value); + /** + * Sets org of the user + * + * @param value org of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setOrg(String value); + /** + * Sets phone of the user + * + * @param value phone of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setPhone(String value); + /** + * Sets picture of the user + * + * @param picture picture of the user + * @return this instance for method chaining + * @deprecated and this function will do nothing + */ UserEditor setPicture(byte[] picture); + /** + * Sets picture of the user + * + * @param picturePath picture of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setPicturePath(String picturePath); + /** + * Sets gender of the user + * + * @param gender of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setGender(Object gender); + /** + * Sets birthyear of the user + * + * @param birthyear of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setBirthyear(int birthyear); + /** + * Sets birthyear of the user + * + * @param birthyear of the user + * @return this instance for method chaining + * @deprecated use {@link ModuleUserProfile.UserProfile#setProperty(String, Object)} instead + */ UserEditor setBirthyear(String birthyear); + /** + * Sets locale of the user + * + * @param locale of the user + * @return this instance for method chaining + * @deprecated and this function will do nothing + */ UserEditor setLocale(String locale); + /** + * Sets country of the user + * + * @param country of the user + * @return this instance for method chaining + * @deprecated todo add location module and its function here + */ UserEditor setCountry(String country); - UserEditor setCity(String country); + /** + * Sets city of the user + * + * @param city of the user + * @return this instance for method chaining + * @deprecated todo add location module and its function here + */ + UserEditor setCity(String city); + /** + * Sets location of the user + * + * @param location of the user + * @return this instance for method chaining + * @deprecated todo add location module and its function here + */ UserEditor setLocation(String location); + /** + * Sets location of the user + * + * @param latitude of the user + * @param longitude of the user + * @return this instance for method chaining + * @deprecated todo add location module and its function here + */ UserEditor setLocation(double latitude, double longitude); + /** + * Clears location values from the user + * + * @return this instance for method chaining + * @deprecated todo add location module and its function here + */ UserEditor optOutFromLocationServices(); + /** + * Increments a user profile property + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#incrementBy(String, int)} instead + */ UserEditor inc(String key, int by); + /** + * Set a user profile property for the multiplied value + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#multiply(String, int)} instead + */ UserEditor mul(String key, double by); + /** + * Set a user profile property for the min value + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#saveMin(String, int)} instead + */ UserEditor min(String key, double value); + /** + * Set a user profile property for the max value + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#saveMax(String, int)} instead + */ UserEditor max(String key, double value); + /** + * Set a user profile property + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#setOnce(String, String)} instead + */ UserEditor setOnce(String key, Object value); + /** + * Pull a value from a user profile property + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#pull(String, String)} instead + */ UserEditor pull(String key, Object value); + /** + * Push a value to a user profile property + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#push(String, String)} instead + */ UserEditor push(String key, Object value); + /** + * Push a unique value to a user profile property + * + * @return UserEditor instance to chain calls + * @deprecated use {@link ModuleUserProfile.UserProfile#pushUnique(String, String)} instead + */ UserEditor pushUnique(String key, Object value); /** @@ -86,5 +258,11 @@ public interface UserEditor { */ UserEditor removeFromCohort(String key); + /** + * Sets birthyear of the user + * + * @return user class instance + * @deprecated use {@link ModuleUserProfile.UserProfile#save()} instead + */ User commit(); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java index c921f3ec1..68668d59d 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java @@ -9,7 +9,7 @@ public enum CoreFeature { Views(1 << 3, ModuleViews::new), CrashReporting(1 << 4, ModuleCrash::new), Location(1 << 5), - UserProfiles(1 << 6), + UserProfiles(1 << 6, ModuleUserProfile::new), /* THESE ARE ONLY HERE AS DOCUMENTATION THEY SHOW WHICH ID'S ARE USED IN ANDROID diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java index 95b307551..abced9ad7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java @@ -67,7 +67,7 @@ private JSONObject doInBackground(String requestData, String customEndpoint, Tra request.endpoint(customEndpoint); //getting connection ready try { - connection = cp.connection(request, null); + connection = cp.connection(request); } catch (IOException e) { L.e("[ImmediateRequestMaker] IOException while preparing remote config update request :[" + e + "]"); return null; @@ -82,7 +82,7 @@ private JSONObject doInBackground(String requestData, String customEndpoint, Tra L.e("[ImmediateRequestMaker] Encountered problem while making a immediate server request, received result was null"); return null; } - + if (code >= 200 && code < 300) { L.d("[ImmediateRequestMaker] Received the following response, :[" + receivedBuffer + "]"); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java new file mode 100644 index 000000000..d653f81a1 --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java @@ -0,0 +1,435 @@ +package ly.count.sdk.java.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; +import ly.count.sdk.java.Countly; +import ly.count.sdk.java.User; +import org.json.JSONException; +import org.json.JSONObject; + +public class ModuleUserProfile extends ModuleBase { + static final String NAME_KEY = "name"; + static final String USERNAME_KEY = "username"; + static final String EMAIL_KEY = "email"; + static final String ORG_KEY = "organization"; + static final String PHONE_KEY = "phone"; + static final String PICTURE_KEY = "picture"; + static final String PICTURE_PATH_KEY = "picturePath"; + static final String GENDER_KEY = "gender"; + static final String BYEAR_KEY = "byear"; + static final String CUSTOM_KEY = "custom"; + boolean isSynced = true; + Map custom; + UserProfile userProfileInterface; + private final Map sets; + private final List ops; + + private static class OpParams { + final String key; + final Object value; + final Op op; + + OpParams(String key, Object value, Op op) { + this.key = key; + this.value = value; + this.op = op; + } + } + + private interface OpFunction { + void apply(JSONObject json, String key, Object value) throws JSONException; + } + + enum Op { + INC((json, key, value) -> { + JSONObject object = json.optJSONObject(key, new JSONObject()); + object.put("$inc", object.optInt("$inc", 0) + (int) value); + json.put(key, object); + }), + MUL((json, key, value) -> { + JSONObject object = json.optJSONObject(key, new JSONObject()); + object.put("$mul", object.optDouble("$mul", 1) * (double) value); + json.put(key, object); + }), + MIN((json, key, value) -> { + JSONObject object = json.optJSONObject(key, new JSONObject()); + object.put("$min", Math.min(object.optDouble("$min", (Double) value), (Double) value)); + json.put(key, object); + }), + MAX((json, key, value) -> { + JSONObject object = json.optJSONObject(key, new JSONObject()); + object.put("$max", Math.max(object.optDouble("$max", (Double) value), (Double) value)); + json.put(key, object); + }), + SET_ONCE(((json, key, value) -> json.put(key, json.optJSONObject(key, new JSONObject()).put("$setOnce", value)))), + PULL((json, key, value) -> json.put(key, json.optJSONObject(key, new JSONObject()).accumulate("$pull", value))), + PUSH((json, key, value) -> json.put(key, json.optJSONObject(key, new JSONObject()).accumulate("$push", value))), + PUSH_UNIQUE((json, key, value) -> json.put(key, json.optJSONObject(key, new JSONObject()).accumulate("$addToSet", value))); + final OpFunction valueTransformer; + + Op(OpFunction valueTransformer) { + this.valueTransformer = valueTransformer; + } + } + + ModuleUserProfile() { + sets = new ConcurrentHashMap<>(); + ops = new ArrayList<>(); + } + + /** + * Gets a value of a property, if it is null returns 'JSONObject.NULL' + * + * @param key to log + * @param value to check + * @return opt out value + */ + private Object optString(String key, Object value) { + if (value == null) { + return JSONObject.NULL; + } + if (!(value instanceof String)) { + L.d("[ModuleUserProfile] optString, value is not a String, thus toString is going to be used for the key:[" + key + "]"); + } + return value.toString(); + } + + /** + * Transforming changes in "sets" into a json contained in "changes" + * + * @param changes + * @throws JSONException + */ + void perform(JSONObject changes) throws JSONException { + for (String key : sets.keySet()) { + Object value = sets.get(key); + switch (key) { + case NAME_KEY: + case USERNAME_KEY: + case EMAIL_KEY: + case ORG_KEY: + case PHONE_KEY: + changes.put(key, optString(key, value)); + break; + case PICTURE_KEY: + if (value == null) { + changes.put(PICTURE_KEY, JSONObject.NULL); + } + break; + case PICTURE_PATH_KEY: + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + changes.put(PICTURE_KEY, JSONObject.NULL); + } else if (value instanceof String) { + if (Utils.isValidURL((String) value)) { + //if it is a valid URL that means the picture is online, and we want to send the link to the server + changes.put(PICTURE_KEY, value); + } else { + //if we get here then that means it is a local file path which we would send over as bytes to the server + changes.put(PICTURE_PATH_KEY, value); + } + } else { + L.e("[UserEditorImpl] Won't set user picturePath (must be String or null)"); + } + break; + case GENDER_KEY: + if (value == null || value instanceof User.Gender) { + changes.put(GENDER_KEY, value == null ? JSONObject.NULL : value.toString()); + } else if (value instanceof String) { + User.Gender gender = User.Gender.fromString((String) value); + if (gender == null) { + L.e("[UserEditorImpl] Cannot parse gender string: " + value + " (must be one of 'F' & 'M')"); + } else { + changes.put(GENDER_KEY, gender.toString()); + } + } else { + L.e("[UserEditorImpl] Won't set user gender (must be of type User.Gender or one of following Strings: 'F', 'M')"); + } + break; + case BYEAR_KEY: + if (value == null || value instanceof Integer) { + changes.put(BYEAR_KEY, value == null ? JSONObject.NULL : value); + } else if (value instanceof String) { + try { + changes.put(BYEAR_KEY, Integer.parseInt((String) value)); + } catch (NumberFormatException e) { + L.e("[UserEditorImpl] user.birthyear must be either Integer or String which can be parsed to Integer" + e); + } + } else { + L.e("[UserEditorImpl] Won't set user birthyear (must be of type Integer or String which can be parsed to Integer)"); + } + break; + default: + performCustomUpdate(key, value, changes); + break; + } + } + + applyOps(changes); + } + + private void applyOps(final JSONObject changes) throws JSONException { + if (!ops.isEmpty() && !changes.has(CUSTOM_KEY)) { + changes.put(CUSTOM_KEY, new JSONObject()); + } + for (OpParams opParam : ops) { + opParam.op.valueTransformer.apply(changes.getJSONObject(CUSTOM_KEY), opParam.key, opParam.value); + } + } + + private void performCustomUpdate(final String key, final Object value, final JSONObject changes) throws JSONException { + if (value == null || value instanceof String || value instanceof Integer || value instanceof Float || value instanceof Double || value instanceof Boolean || value instanceof Object[]) { + if (!changes.has(CUSTOM_KEY)) { + changes.put(CUSTOM_KEY, new JSONObject()); + } + changes.getJSONObject(CUSTOM_KEY).put(key, value); + if (value == null) { + custom.remove(key); + } else { + custom.put(key, value); + } + } else { + L.e("[UserEditorImpl] performCustomUpdate, Type of value " + value + " '" + value.getClass().getSimpleName() + "' is not supported yet, thus user property is not stored"); + } + } + + /** + * Returns &user_details= prefixed url to add to request data when making request to server + * + * @return a String user_details url part with provided user data + */ + private Params prepareRequestParamsForUserProfile() { + isSynced = true; + Params params = new Params(); + final JSONObject json = new JSONObject(); + perform(json); + params.add("user_details", json.toString()); + return params; + } + + /** + * Atomic modifications on custom user property. + * + * @param key String with property name to modify + * @param value String value to use in modification + * @param mod String with modification command + */ + protected void modifyCustomData(String key, Object value, Op mod) { + ops.add(new OpParams(key, value, mod)); + isSynced = false; + } + + /** + * This mainly performs the filtering of provided values + * This single call would be used for both predefined properties and custom user properties + * + * @param data Map with user data + */ + protected void setPropertiesInternal(@Nonnull Map data) { + if (data.isEmpty()) { + L.w("[ModuleUserProfile] setPropertiesInternal, no data was provided"); + return; + } + + sets.putAll(data); + isSynced = false; + } + + protected void saveInternal() { + if (isSynced) { + L.d("[ModuleUserProfile] saveInternal, nothing to save returning"); + return; + } + Params generatedParams = prepareRequestParamsForUserProfile(); + L.d("[ModuleUserProfile] saveInternal, generated params [" + generatedParams + "]"); + ModuleRequests.pushAsync(internalConfig, new Request(generatedParams)); + clearInternal(); + } + + protected void clearInternal() { + L.d("[ModuleUserProfile] clearInternal"); + + sets.clear(); + ops.clear(); + isSynced = true; + } + + @Override + public void init(InternalConfig internalConfig) { + super.init(internalConfig); + userProfileInterface = new UserProfile(); + } + + @Override + public void initFinished(InternalConfig internalConfig) { + super.initFinished(internalConfig); + } + + @Override + public void stop(InternalConfig config, boolean clearData) { + userProfileInterface = null; + } + + public class UserProfile { + /** + * Increment custom property value by 1. + * + * @param key String with property name to increment + */ + public void increment(String key) { + synchronized (Countly.instance()) { + modifyCustomData(key, 1, Op.INC); + } + } + + /** + * Increment custom property value by provided value. + * + * @param key String with property name to increment + * @param value int value by which to increment + */ + public void incrementBy(String key, int value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.INC); + } + } + + /** + * Multiply custom property value by provided value. + * + * @param key String with property name to multiply + * @param value int value by which to multiply + */ + public void multiply(String key, int value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.MUL); + } + } + + /** + * Save maximal value between existing and provided. + * + * @param key String with property name to check for max + * @param value int value to check for max + */ + public void saveMax(String key, int value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.MAX); + } + } + + /** + * Save minimal value between existing and provided. + * + * @param key String with property name to check for min + * @param value int value to check for min + */ + public void saveMin(String key, int value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.MIN); + } + } + + /** + * Set value only if property does not exist yet + * + * @param key String with property name to set + * @param value String value to set + */ + public void setOnce(String key, String value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.SET_ONCE); + } + } + + /* Create array property, if property does not exist and add value to array + * You can only use it on array properties or properties that do not exist yet + * @param key String with property name for array property + * @param value String with value to add to array + */ + public void push(String key, String value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.PUSH); + } + } + + /* Create array property, if property does not exist and add value to array, only if value is not yet in the array + * You can only use it on array properties or properties that do not exist yet + * @param key String with property name for array property + * @param value String with value to add to array + */ + public void pushUnique(String key, String value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.PUSH_UNIQUE); + } + } + + /* Create array property, if property does not exist and remove value from array + * You can only use it on array properties or properties that do not exist yet + * @param key String with property name for array property + * @param value String with value to remove from array + */ + public void pull(String key, String value) { + synchronized (Countly.instance()) { + modifyCustomData(key, value, Op.PULL); + } + } + + /** + * Set a single user property. It can be either a custom one or one of the predefined ones. + * + * @param key the key for the user property + * @param value the value for the user property to be set. The value should be the allowed data type. + */ + public void setProperty(String key, Object value) { + synchronized (Countly.instance()) { + L.i("[UserProfile] Calling 'setProperty'"); + + Map data = new ConcurrentHashMap<>(); + data.put(key, value); + + setPropertiesInternal(data); + } + } + + /** + * Provide a map of user properties to set. + * Those can be either custom user properties or predefined user properties + * + * @param data Map of user properties to set + */ + public void setProperties(Map data) { + synchronized (Countly.instance()) { + L.i("[UserProfile] Calling 'setProperties'"); + + if (data == null) { + L.i("[UserProfile] Provided data can not be 'null'"); + return; + } + setPropertiesInternal(data); + } + } + + /** + * Send provided values to server + */ + public void save() { + synchronized (Countly.instance()) { + L.i("[UserProfile] Calling 'save'"); + saveInternal(); + } + } + + /** + * Clear queued operations / modifications + */ + public void clear() { + synchronized (Countly.instance()) { + L.i("[UserProfile] Calling 'clear'"); + clearInternal(); + } + } + } +} diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java index 806030cbf..a805b9e3e 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java @@ -62,6 +62,7 @@ protected static void registerDefaultModuleMappings() { moduleMappings.put(CoreFeature.Feedback.getIndex(), ModuleFeedback.class); moduleMappings.put(CoreFeature.Events.getIndex(), ModuleEvents.class); moduleMappings.put(CoreFeature.RemoteConfig.getIndex(), ModuleRemoteConfig.class); + moduleMappings.put(CoreFeature.UserProfiles.getIndex(), ModuleUserProfile.class); } /** @@ -399,6 +400,16 @@ public ModuleRemoteConfig.RemoteConfig remoteConfig() { return module(ModuleRemoteConfig.class).remoteConfigInterface; } + public ModuleUserProfile.UserProfile userProfile() { + //todo is this needed? + //if (!hasConsentForFeature(CoreFeature.UserProfiles)) { + // L.v("[SDKCore] remoteConfig, RemoteConfig feature has no consent, returning null"); + // return null; + //} + + return module(ModuleUserProfile.class).userProfileInterface; + } + /** * Get current {@link SessionImpl} or create new one if current is {@code null}. * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java index ff554ab76..d0b3b0879 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java @@ -35,7 +35,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import ly.count.sdk.java.User; import org.json.JSONObject; /** @@ -117,11 +116,10 @@ public HttpURLConnection openConnection(String url, String params, boolean using * set SSL context, calculate and add checksum, load and send user picture if needed. * * @param request request to send - * @param user user to check for picture * @return connection, not {@link HttpURLConnection} yet * @throws IOException from {@link HttpURLConnection} in case of error */ - HttpURLConnection connection(final Request request, final User user) throws IOException { + HttpURLConnection connection(final Request request) throws IOException { String endpoint = request.params.remove(Request.ENDPOINT); if (!request.params.has("device_id") && config.getDeviceId() != null) { @@ -134,7 +132,7 @@ HttpURLConnection connection(final Request request, final User user) throws IOEx } String path = config.getServerURL().toString() + endpoint; - String picturePathValue = request.params.remove(UserEditorImpl.PICTURE_PATH); + String picturePathValue = request.params.remove(ModuleUserProfile.PICTURE_PATH_KEY); boolean usingGET = !config.isHTTPPostForced() && request.isGettable(config.getServerURL()) && Utils.isEmptyOrNull(picturePathValue); if (!usingGET && !Utils.isEmptyOrNull(picturePathValue)) { @@ -159,7 +157,7 @@ HttpURLConnection connection(final Request request, final User user) throws IOEx PrintWriter writer = null; try { L.d("[network] Picture path value " + picturePathValue); - byte[] pictureByteData = picturePathValue == null ? null : getPictureDataFromGivenValue(user, picturePathValue); + byte[] pictureByteData = picturePathValue == null ? null : getPictureDataFromGivenValue(picturePathValue); if (pictureByteData != null) { String boundary = Long.toHexString(System.currentTimeMillis()); @@ -236,34 +234,22 @@ void addMultipart(OutputStream output, PrintWriter writer, String boundary, Stri /** * Returns valid picture information - * If we have the bytes, give them - * Otherwise load them from disk + * Load the picture from disk * - * @param user - * @param picture - * @return + * @param picturePath path to the picture + * @return byte array of the picture */ - byte[] getPictureDataFromGivenValue(User user, String picture) { - if (user == null) { - return null; - } - + byte[] getPictureDataFromGivenValue(String picturePath) { byte[] data = null; - if (UserEditorImpl.PICTURE_IN_USER_PROFILE.equals(picture)) { - //if the value is this special value then we know that we will send over bytes that are already provided by the integrator - //those stored bytes are already in a internal data structure, use them - data = user.picture(); - } else { - //otherwise we assume it is a local path, and we try to read it from disk - try { - File file = new File(picture); - if (!file.exists()) { - return null; - } - data = Files.readAllBytes(file.toPath()); - } catch (Throwable t) { - L.w("[Transport] getPictureDataFromGivenValue, Error while reading picture from disk " + t); + //we assume it is a local path, and we try to read it from disk + try { + File file = new File(picturePath); + if (!file.exists()) { + return null; } + data = Files.readAllBytes(file.toPath()); + } catch (Throwable t) { + L.w("[Transport] getPictureDataFromGivenValue, Error while reading picture from disk " + t); } return data; @@ -318,7 +304,7 @@ public Boolean send() { Class requestOwner = request.owner(); request.params.remove(Request.MODULE); - connection = connection(request, SDKCore.instance.user()); + connection = connection(request); connection.connect(); int code = connection.getResponseCode(); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java index ae74f1191..a8fcef335 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java @@ -1,355 +1,70 @@ package ly.count.sdk.java.internal; import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import ly.count.sdk.java.Countly; import ly.count.sdk.java.User; import ly.count.sdk.java.UserEditor; import org.json.JSONException; -import org.json.JSONObject; public class UserEditorImpl implements UserEditor { - private Log L = null; - - static class Op { - static final String INC = "$inc"; - static final String MUL = "$mul"; - static final String MIN = "$min"; - static final String MAX = "$max"; - static final String SET_ONCE = "$setOnce"; - static final String PULL = "$pull"; - static final String PUSH = "$push"; - static final String PUSH_UNIQUE = "$addToSet"; - - final String op; - final String key; - final Object value; - - Op(String op, String key, Object value) { - this.op = op; - this.key = key; - this.value = value; - } - - public void apply(JSONObject json) throws JSONException { - JSONObject object; - switch (op) { - case INC: - case MUL: - object = json.optJSONObject(key); - if (object == null) { - object = new JSONObject(); - } - if (op.equals(INC)) { - int n = object.optInt(op, 0); - object.put(op, n + (int) value); - } else { - double n = object.optDouble(op, 1); - object.put(op, n * (double) value); - } - json.put(key, object); - break; - case MIN: - case MAX: - object = json.optJSONObject(key); - if (object == null) { - object = new JSONObject(); - } - if (object.has(op)) { - object.put(op, op.equals(MIN) ? Math.min(object.getDouble(op), (Double) value) : Math.max(object.getDouble(op), (Double) value)); - } else { - object.put(op, value); - } - json.put(key, object); - break; - case SET_ONCE: - object = json.optJSONObject(key); - if (object == null) { - object = new JSONObject(); - } - object.put(op, value); - json.put(key, object); - break; - case PULL: - case PUSH: - case PUSH_UNIQUE: - object = json.optJSONObject(key); - if (object == null) { - object = new JSONObject(); - } - object.accumulate(op, value); - json.put(key, object); - break; - } - } - } - - static final String NAME = "name"; - static final String USERNAME = "username"; - static final String EMAIL = "email"; - static final String ORG = "org"; - static final String PHONE = "phone"; - static final String PICTURE = "picture"; - public static final String PICTURE_PATH = "picturePath"; - public static final String PICTURE_IN_USER_PROFILE = "[CLY]_USER_PROFILE_PICTURE"; - static final String GENDER = "gender"; - static final String BIRTHYEAR = "byear"; - static final String LOCALE = "locale"; - static final String COUNTRY = "country"; - static final String CITY = "city"; - static final String LOCATION = "location"; - static final String CUSTOM = "custom"; + private final Log L; private final UserImpl user; - private final Map sets; - private final List ops; UserEditorImpl(UserImpl user, Log logger) { this.L = logger; this.user = user; - this.sets = new HashMap<>(); - this.ops = new ArrayList<>(); - } - - /** - * Transforming changes in "sets" into a json contained in "changes" - * - * @param changes - * @throws JSONException - */ - void perform(JSONObject changes) throws JSONException { - for (String key : sets.keySet()) { - Object value = sets.get(key); - switch (key) { - case NAME: - if (value == null || value instanceof String) { - user.name = (String) value; - } else { - L.w("user.name will be cast to String"); - user.name = value.toString(); - } - changes.put(NAME, value == null ? JSONObject.NULL : user.name); - break; - case USERNAME: - if (value == null || value instanceof String) { - user.username = (String) value; - } else { - L.w("user.username will be cast to String"); - user.username = value.toString(); - } - changes.put(USERNAME, value == null ? JSONObject.NULL : user.username); - break; - case EMAIL: - if (value == null || value instanceof String) { - user.email = (String) value; - } else { - L.w("user.email will be cast to String"); - user.email = value.toString(); - } - changes.put(EMAIL, value == null ? JSONObject.NULL : user.email); - break; - case ORG: - if (value == null || value instanceof String) { - user.org = (String) value; - } else { - L.w("user.org will be cast to String"); - user.org = value.toString(); - } - changes.put(ORG, value == null ? JSONObject.NULL : user.org); - break; - case PHONE: - if (value == null || value instanceof String) { - user.phone = (String) value; - } else { - L.w("user.phone will be cast to String"); - user.phone = value.toString(); - } - changes.put(PHONE, value == null ? JSONObject.NULL : user.phone); - break; - case PICTURE: - //if we get here, that means that the dev gave us bytes for the picture - if (value == null) { - //there is an indication that the picture should be erased server side - user.picture = null; - user.picturePath = null; - changes.put(PICTURE, JSONObject.NULL); - } else if (value instanceof byte[]) { - user.picture = (byte[]) value; - //set a special value to indicate that the picture information is already stored in memory - changes.put(PICTURE_PATH, PICTURE_IN_USER_PROFILE); - } else { - L.e("[UserEditorImpl] Won't set user picture (must be of type byte[])"); - } - break; - case PICTURE_PATH: - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - //there is an indication that the picture should be erased server side - user.picture = null; - user.picturePath = null; - changes.put(PICTURE, JSONObject.NULL); - } else if (value instanceof String) { - if (Utils.isValidURL((String) value)) { - //if it is a valid URL that means the picture is online, and we want to send the link to the server - changes.put(PICTURE, value); - } else { - //if we get here then that means it is a local file path which we would send over as bytes to the server - changes.put(PICTURE_PATH, value); - } - user.picturePath = value.toString(); - } else { - L.e("[UserEditorImpl] Won't set user picturePath (must be String or null)"); - } - break; - case GENDER: - if (value == null || value instanceof User.Gender) { - user.gender = (User.Gender) value; - changes.put(GENDER, user.gender == null ? JSONObject.NULL : user.gender.toString()); - } else if (value instanceof String) { - User.Gender gender = User.Gender.fromString((String) value); - if (gender == null) { - L.e("[UserEditorImpl] Cannot parse gender string: " + value + " (must be one of 'F' & 'M')"); - } else { - user.gender = gender; - changes.put(GENDER, user.gender.toString()); - } - } else { - L.e("[UserEditorImpl] Won't set user gender (must be of type User.Gender or one of following Strings: 'F', 'M')"); - } - break; - case BIRTHYEAR: - if (value == null || value instanceof Integer) { - user.birthyear = (Integer) value; - changes.put(BIRTHYEAR, value == null ? JSONObject.NULL : user.birthyear); - } else if (value instanceof String) { - try { - user.birthyear = Integer.parseInt((String) value); - changes.put(BIRTHYEAR, user.birthyear); - } catch (NumberFormatException e) { - L.e("[UserEditorImpl] user.birthyear must be either Integer or String which can be parsed to Integer" + e); - } - } else { - L.e("[UserEditorImpl] Won't set user birthyear (must be of type Integer or String which can be parsed to Integer)"); - } - break; - case LOCALE: - if (value == null || value instanceof String) { - user.locale = (String) value; - changes.put(LOCALE, value == null ? JSONObject.NULL : user.locale); - } - break; - case COUNTRY: - if (value == null || value instanceof String) { - user.country = (String) value; - changes.put(COUNTRY, value == null ? JSONObject.NULL : user.country); - } - break; - case CITY: - if (value == null || value instanceof String) { - user.city = (String) value; - changes.put(CITY, value == null ? JSONObject.NULL : user.city); - } - break; - case LOCATION: - if (value == null || value instanceof String) { - user.location = (String) value; - changes.put(LOCATION, value == null ? JSONObject.NULL : user.location); - } - break; - default: - performCustomUpdate(key, value, changes); - break; - } - } - - applyOps(changes); - } - - private void applyOps(final JSONObject changes) throws JSONException { - if (!ops.isEmpty() && !changes.has(CUSTOM)) { - changes.put(CUSTOM, new JSONObject()); - } - for (Op op : ops) { - op.apply(changes.getJSONObject(CUSTOM)); - } - } - - private void performCustomUpdate(final String key, final Object value, final JSONObject changes) throws JSONException { - if (value == null || value instanceof String || value instanceof Integer || value instanceof Float || value instanceof Double || value instanceof Boolean || value instanceof Object[]) { - if (!changes.has(CUSTOM)) { - changes.put(CUSTOM, new JSONObject()); - } - changes.getJSONObject(CUSTOM).put(key, value); - if (value == null) { - user.custom.remove(key); - } else { - user.custom.put(key, value); - } - } else { - L.e("[UserEditorImpl] performCustomUpdate, Type of value " + value + " '" + value.getClass().getSimpleName() + "' is not supported yet, thus user property is not stored"); - } } @Override public UserEditor set(String key, Object value) { - sets.put(key, value); + Countly.instance().userProfile().setProperty(key, value); return this; } @Override @SuppressWarnings("unchecked") public UserEditor setCustom(String key, Object value) { - if (!sets.containsKey(CUSTOM)) { - sets.put(CUSTOM, new HashMap()); - } - Map custom = (Map) sets.get(CUSTOM); - custom.put(key, value); - return this; - } - - @SuppressWarnings("unchecked") - private UserEditor setCustomOp(String op, String key, Object value) { - ops.add(new Op(op, key, value)); + Countly.instance().userProfile().setProperty(key, value); return this; } @Override public UserEditor setName(String value) { L.d("setName: value = " + value); - - return set(NAME, value); + return set(ModuleUserProfile.NAME_KEY, value); } @Override public UserEditor setUsername(String value) { L.d("setUsername: value = " + value); - return set(USERNAME, value); + return set(ModuleUserProfile.USERNAME_KEY, value); } @Override public UserEditor setEmail(String value) { L.d("setEmail: value = " + value); - return set(EMAIL, value); + return set(ModuleUserProfile.EMAIL_KEY, value); } @Override public UserEditor setOrg(String value) { L.d("setOrg: value = " + value); - return set(ORG, value); + return set(ModuleUserProfile.ORG_KEY, value); } @Override public UserEditor setPhone(String value) { L.d("setPhone: value = " + value); - return set(PHONE, value); + return set(ModuleUserProfile.PHONE_KEY, value); } //we set the bytes for the local picture @Override public UserEditor setPicture(byte[] picture) { L.d("setPicture: picture = " + picture); - return set(PICTURE, picture); + //this will deprecate + return this; } //we set the url for either the online picture or a local path picture @@ -358,7 +73,7 @@ public UserEditor setPicturePath(String picturePath) { L.d("[UserEditorImpl] setPicturePath, picturePath = " + picturePath); if (picturePath == null || Utils.isValidURL(picturePath) || (new File(picturePath)).isFile()) { //if it is a thing we can use, continue - return set(PICTURE_PATH, picturePath); + return set(ModuleUserProfile.PICTURE_PATH_KEY, picturePath); } L.w("[UserEditorImpl] setPicturePath, picturePath is not a valid file path or url"); return this; @@ -367,32 +82,34 @@ public UserEditor setPicturePath(String picturePath) { @Override public UserEditor setGender(Object gender) { L.d("setGender: gender = " + gender); - return set(GENDER, gender); + return set(ModuleUserProfile.GENDER_KEY, gender); } @Override public UserEditor setBirthyear(int birthyear) { L.d("setBirthyear: birthyear = " + birthyear); - return set(BIRTHYEAR, birthyear); + return set(ModuleUserProfile.BYEAR_KEY, birthyear); } @Override public UserEditor setBirthyear(String birthyear) { L.d("setBirthyear: birthyear = " + birthyear); - return set(BIRTHYEAR, birthyear); + return set(ModuleUserProfile.BYEAR_KEY, birthyear); } @Override public UserEditor setLocale(String locale) { L.d("setLocale: locale = " + locale); - return set(LOCALE, locale); + //todo this is not working on server side deprecate + return this; } @Override public UserEditor setCountry(String country) { L.d("setCountry: country = " + country); if (SDKCore.enabled(CoreFeature.Location)) { - return set(COUNTRY, country); + //todo when location module added add its function here + return this; } else { return this; } @@ -402,7 +119,8 @@ public UserEditor setCountry(String country) { public UserEditor setCity(String city) { L.d("setCity: city = " + city); if (SDKCore.enabled(CoreFeature.Location)) { - return set(CITY, city); + //todo when location module added add its function here + return this; } else { return this; } @@ -415,7 +133,8 @@ public UserEditor setLocation(String location) { String[] comps = location.split(","); if (comps.length == 2) { try { - return set(LOCATION, Double.valueOf(comps[0]) + "," + Double.valueOf(comps[1])); + //todo when location module added add its function here + return this; } catch (Throwable t) { L.e("[UserEditorImpl] Invalid location format: " + location + " " + t); return this; @@ -425,7 +144,8 @@ public UserEditor setLocation(String location) { return this; } } else { - return set(LOCATION, null); + //todo when location module added add its function here + return this; } } @@ -433,7 +153,8 @@ public UserEditor setLocation(String location) { public UserEditor setLocation(double latitude, double longitude) { L.d("setLocation: latitude = " + latitude + " longitude" + longitude); if (SDKCore.enabled(CoreFeature.Location)) { - return set(LOCATION, latitude + "," + longitude); + //todo when location module added add its function here + return this; } else { return this; } @@ -442,33 +163,66 @@ public UserEditor setLocation(double latitude, double longitude) { @Override public UserEditor optOutFromLocationServices() { L.d("optOutFromLocationServices"); - return set(COUNTRY, "").set(CITY, "").set(LOCATION, ""); + //todo when location module added add its function here + return this; } @Override public UserEditor inc(String key, int by) { L.d("inc: key " + key + " by " + by); - return setCustomOp(Op.INC, key, by); + Countly.instance().userProfile().incrementBy(key, by); + return this; } + /** + * now value is mapped to int + * + * @param key + * @param value + * @return + */ @Override public UserEditor mul(String key, double by) { L.d("mul: key " + key + " by " + by); - return setCustomOp(Op.MUL, key, by); + Countly.instance().userProfile().multiply(key, Double.valueOf(by).intValue()); + return this; } + /** + * now value is mapped to int + * + * @param key + * @param value + * @return + */ @Override public UserEditor min(String key, double value) { L.d("min: key " + key + " value " + value); - return setCustomOp(Op.MIN, key, value); + Countly.instance().userProfile().saveMin(key, Double.valueOf(value).intValue()); + return this; } + /** + * now value is mapped to int + * + * @param key + * @param value + * @return + */ @Override public UserEditor max(String key, double value) { L.d("max: key " + key + " value " + value); - return setCustomOp(Op.MAX, key, value); + Countly.instance().userProfile().saveMax(key, Double.valueOf(value).intValue()); + return this; } + /** + * Now value is mapped to string + * + * @param key + * @param value + * @return + */ @Override public UserEditor setOnce(String key, Object value) { L.d("setOnce: key " + key + " value " + value); @@ -476,10 +230,18 @@ public UserEditor setOnce(String key, Object value) { L.e("[UserEditorImpl] $setOnce operation operand cannot be null: key " + key); return this; } else { - return setCustomOp(Op.SET_ONCE, key, value); + Countly.instance().userProfile().setOnce(key, value.toString()); + return this; } } + /** + * Now value is mapped to string + * + * @param key + * @param value + * @return + */ @Override public UserEditor pull(String key, Object value) { L.d("pull: key " + key + " value " + value); @@ -487,10 +249,18 @@ public UserEditor pull(String key, Object value) { L.e("[UserEditorImpl] $pull operation operand cannot be null: key " + key); return this; } else { - return setCustomOp(Op.PULL, key, value); + Countly.instance().userProfile().pull(key, value.toString()); + return this; } } + /** + * Now value is mapped to string + * + * @param key + * @param value + * @return + */ @Override public UserEditor push(String key, Object value) { L.d("push: key " + key + " value " + value); @@ -498,10 +268,18 @@ public UserEditor push(String key, Object value) { L.e("[UserEditorImpl] $push operation operand cannot be null: key " + key); return this; } else { - return setCustomOp(Op.PUSH, key, value); + Countly.instance().userProfile().push(key, value.toString()); + return this; } } + /** + * Now value is mapped to string + * + * @param key + * @param value + * @return + */ @Override public UserEditor pushUnique(String key, Object value) { L.d("pushUnique: key " + key + " value " + value); @@ -509,7 +287,8 @@ public UserEditor pushUnique(String key, Object value) { L.e("[UserEditorImpl] pushUnique / $addToSet operation operand cannot be null: key " + key); return this; } else { - return setCustomOp(Op.PUSH_UNIQUE, key, value); + Countly.instance().userProfile().pushUnique(key, value.toString()); + return this; } } @@ -533,48 +312,18 @@ public User commit() { return null; } - if (SDKCore.instance != null && SDKCore.instance.config.isBackendModeEnabled()) { + if (SDKCore.instance.config.isBackendModeEnabled()) { L.w("commit: Skipping user detail, backend mode is enabled!"); return null; } try { - final JSONObject changes = new JSONObject(); - - perform(changes); - - Storage.push(SDKCore.instance.config, user); - - ModuleRequests.injectParams(SDKCore.instance.config, params -> { - if (changes.has(PICTURE_PATH)) { - try { - params.add(PICTURE_PATH, changes.getString(PICTURE_PATH)); - changes.remove(PICTURE_PATH); - } catch (JSONException e) { - L.w("Won't send picturePath" + e); - } - } - if (changes.has(LOCALE) && user.locale != null) { - params.add("locale", user.locale); - } - if (changes.has(COUNTRY) && user.country != null) { - params.add("country_code", user.country); - } - if (changes.has(CITY) && user.city != null) { - params.add("city", user.city); - } - if (changes.has(LOCATION) && user.location != null) { - params.add("location", user.location); - } - params.add("user_details", changes.toString()); - }); + Countly.instance().userProfile().save(); + Storage.push(SDKCore.instance.config, user); // todo this is not need it is for another task } catch (JSONException e) { L.e("[UserEditorImpl] Exception while committing changes to User profile" + e); } - sets.clear(); - ops.clear(); - return user; } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java index 103c28d49..741d3d270 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java @@ -116,7 +116,8 @@ public void setPicture_binaryData() { validatePictureAndPath(null, imgData); Countly.session().end(); - validatePictureInRQ("{}", UserEditorImpl.PICTURE_IN_USER_PROFILE); + //todo should we? + //validatePictureInRQ("{}", UserEditorImpl.PICTURE_IN_USER_PROFILE); } /** From aa9220f5895d0915ae2f4e2fd4228b6e664b45bf Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 17 Nov 2023 14:52:07 +0300 Subject: [PATCH 2/5] fix: checkup a little comments --- .../sdk/java/internal/UserEditorImpl.java | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java index a8fcef335..b5bc181e9 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java @@ -100,7 +100,6 @@ public UserEditor setBirthyear(String birthyear) { @Override public UserEditor setLocale(String locale) { L.d("setLocale: locale = " + locale); - //todo this is not working on server side deprecate return this; } @@ -174,13 +173,6 @@ public UserEditor inc(String key, int by) { return this; } - /** - * now value is mapped to int - * - * @param key - * @param value - * @return - */ @Override public UserEditor mul(String key, double by) { L.d("mul: key " + key + " by " + by); @@ -202,13 +194,6 @@ public UserEditor min(String key, double value) { return this; } - /** - * now value is mapped to int - * - * @param key - * @param value - * @return - */ @Override public UserEditor max(String key, double value) { L.d("max: key " + key + " value " + value); @@ -216,13 +201,6 @@ public UserEditor max(String key, double value) { return this; } - /** - * Now value is mapped to string - * - * @param key - * @param value - * @return - */ @Override public UserEditor setOnce(String key, Object value) { L.d("setOnce: key " + key + " value " + value); @@ -235,13 +213,6 @@ public UserEditor setOnce(String key, Object value) { } } - /** - * Now value is mapped to string - * - * @param key - * @param value - * @return - */ @Override public UserEditor pull(String key, Object value) { L.d("pull: key " + key + " value " + value); @@ -254,13 +225,6 @@ public UserEditor pull(String key, Object value) { } } - /** - * Now value is mapped to string - * - * @param key - * @param value - * @return - */ @Override public UserEditor push(String key, Object value) { L.d("push: key " + key + " value " + value); @@ -272,14 +236,7 @@ public UserEditor push(String key, Object value) { return this; } } - - /** - * Now value is mapped to string - * - * @param key - * @param value - * @return - */ + @Override public UserEditor pushUnique(String key, Object value) { L.d("pushUnique: key " + key + " value " + value); From 0b25da90dcc18d1c229a43ad2fc1395597fe0c24 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 20 Nov 2023 10:37:35 +0300 Subject: [PATCH 3/5] fix: revert picture byte array changes --- CHANGELOG.md | 4 +- .../sdk/java/internal/ModuleUserProfile.java | 5 +++ .../ly/count/sdk/java/internal/Transport.java | 44 ++++++++++++------- .../sdk/java/internal/UserEditorImpl.java | 5 +-- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e33bcf8..2819dc51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ ## XX.XX.XX -* !! Major breaking change !! The following methods and their functionality are deprecated from the "UserEditor" interface and will not function anymore: - * "picture(byte[])" +* !! Major breaking change !! The following method and its functionality is deprecated from the "UserEditor" interface and will not function anymore: * "setLocale(String)" * Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. @@ -32,6 +31,7 @@ * "setOrg(String)" instead use "Countly::userProfile::setProperty" via "instance()" call * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call + * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call ## 23.10.1 diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java index d653f81a1..9df1c5897 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java @@ -21,6 +21,7 @@ public class ModuleUserProfile extends ModuleBase { static final String GENDER_KEY = "gender"; static final String BYEAR_KEY = "byear"; static final String CUSTOM_KEY = "custom"; + static final String PICTURE_IN_USER_PROFILE = "[CLY]_USER_PROFILE_PICTURE"; boolean isSynced = true; Map custom; UserProfile userProfileInterface; @@ -117,6 +118,10 @@ void perform(JSONObject changes) throws JSONException { case PICTURE_KEY: if (value == null) { changes.put(PICTURE_KEY, JSONObject.NULL); + } else if (value instanceof byte[]) { + internalConfig.sdk.user().picture = (byte[]) value; + //set a special value to indicate that the picture information is already stored in memory + changes.put(PICTURE_PATH_KEY, PICTURE_IN_USER_PROFILE); } break; case PICTURE_PATH_KEY: diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java index d0b3b0879..346f49d8f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java @@ -35,6 +35,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import ly.count.sdk.java.User; import org.json.JSONObject; /** @@ -116,10 +117,11 @@ public HttpURLConnection openConnection(String url, String params, boolean using * set SSL context, calculate and add checksum, load and send user picture if needed. * * @param request request to send + * @param user user to check for picture * @return connection, not {@link HttpURLConnection} yet * @throws IOException from {@link HttpURLConnection} in case of error */ - HttpURLConnection connection(final Request request) throws IOException { + HttpURLConnection connection(final Request request, final User user) throws IOException { String endpoint = request.params.remove(Request.ENDPOINT); if (!request.params.has("device_id") && config.getDeviceId() != null) { @@ -157,7 +159,7 @@ HttpURLConnection connection(final Request request) throws IOException { PrintWriter writer = null; try { L.d("[network] Picture path value " + picturePathValue); - byte[] pictureByteData = picturePathValue == null ? null : getPictureDataFromGivenValue(picturePathValue); + byte[] pictureByteData = picturePathValue == null ? null : getPictureDataFromGivenValue(user, picturePathValue); if (pictureByteData != null) { String boundary = Long.toHexString(System.currentTimeMillis()); @@ -234,22 +236,34 @@ void addMultipart(OutputStream output, PrintWriter writer, String boundary, Stri /** * Returns valid picture information - * Load the picture from disk + * If we have the bytes, give them + * Otherwise load them from disk * - * @param picturePath path to the picture - * @return byte array of the picture + * @param user + * @param picture + * @return */ - byte[] getPictureDataFromGivenValue(String picturePath) { + byte[] getPictureDataFromGivenValue(User user, String picture) { + if (user == null) { + return null; + } + byte[] data = null; - //we assume it is a local path, and we try to read it from disk - try { - File file = new File(picturePath); - if (!file.exists()) { - return null; + if (ModuleUserProfile.PICTURE_IN_USER_PROFILE.equals(picture)) { + //if the value is this special value then we know that we will send over bytes that are already provided by the integrator + //those stored bytes are already in a internal data structure, use them + data = user.picture(); + } else { + //otherwise we assume it is a local path, and we try to read it from disk + try { + File file = new File(picture); + if (!file.exists()) { + return null; + } + data = Files.readAllBytes(file.toPath()); + } catch (Throwable t) { + L.w("[Transport] getPictureDataFromGivenValue, Error while reading picture from disk " + t); } - data = Files.readAllBytes(file.toPath()); - } catch (Throwable t) { - L.w("[Transport] getPictureDataFromGivenValue, Error while reading picture from disk " + t); } return data; @@ -304,7 +318,7 @@ public Boolean send() { Class requestOwner = request.owner(); request.params.remove(Request.MODULE); - connection = connection(request); + connection = connection(request, SDKCore.instance.user()); connection.connect(); int code = connection.getResponseCode(); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java index b5bc181e9..9e57c8718 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java @@ -63,8 +63,7 @@ public UserEditor setPhone(String value) { @Override public UserEditor setPicture(byte[] picture) { L.d("setPicture: picture = " + picture); - //this will deprecate - return this; + return set(ModuleUserProfile.PICTURE_KEY, picture); } //we set the url for either the online picture or a local path picture @@ -236,7 +235,7 @@ public UserEditor push(String key, Object value) { return this; } } - + @Override public UserEditor pushUnique(String key, Object value) { L.d("pushUnique: key " + key + " value " + value); From 9f320e5e1f1214fd27529d7875ae738802e1a7f3 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 20 Nov 2023 11:24:49 +0300 Subject: [PATCH 4/5] fix: rework with images --- CHANGELOG.md | 1 + .../java/internal/ImmediateRequestMaker.java | 2 +- .../sdk/java/internal/ModuleUserProfile.java | 60 +++++++++---- .../ly/count/sdk/java/internal/SDKCore.java | 7 +- .../sdk/java/internal/UserEditorImpl.java | 14 +-- .../sdk/java/internal/UserEditorTests.java | 87 +++++++++---------- 6 files changed, 96 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 140cd0b7f..a938c3cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. * Fixed a bug where setting custom user properties would not work. +* Fixed a bug where setting organization of the user would not work. * Deprecated "Countly::backendMode()" call, use "Countly::backendM" instead via "instance()" call. * The following methods are deprecated from the "UserEditor" interface: diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java index abced9ad7..5d8bf8f2c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ImmediateRequestMaker.java @@ -67,7 +67,7 @@ private JSONObject doInBackground(String requestData, String customEndpoint, Tra request.endpoint(customEndpoint); //getting connection ready try { - connection = cp.connection(request); + connection = cp.connection(request, null); } catch (IOException e) { L.e("[ImmediateRequestMaker] IOException while preparing remote config update request :[" + e + "]"); return null; diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java index 9df1c5897..e014e968d 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java @@ -1,9 +1,9 @@ package ly.count.sdk.java.internal; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import ly.count.sdk.java.Countly; import ly.count.sdk.java.User; @@ -23,7 +23,6 @@ public class ModuleUserProfile extends ModuleBase { static final String CUSTOM_KEY = "custom"; static final String PICTURE_IN_USER_PROFILE = "[CLY]_USER_PROFILE_PICTURE"; boolean isSynced = true; - Map custom; UserProfile userProfileInterface; private final Map sets; private final List ops; @@ -77,7 +76,7 @@ enum Op { } ModuleUserProfile() { - sets = new ConcurrentHashMap<>(); + sets = new HashMap<>(); // keys should be nullable ops = new ArrayList<>(); } @@ -118,6 +117,8 @@ void perform(JSONObject changes) throws JSONException { case PICTURE_KEY: if (value == null) { changes.put(PICTURE_KEY, JSONObject.NULL); + internalConfig.sdk.user().picturePath = null; + internalConfig.sdk.user().picture = null; } else if (value instanceof byte[]) { internalConfig.sdk.user().picture = (byte[]) value; //set a special value to indicate that the picture information is already stored in memory @@ -127,6 +128,8 @@ void perform(JSONObject changes) throws JSONException { case PICTURE_PATH_KEY: if (value == null || (value instanceof String && ((String) value).isEmpty())) { changes.put(PICTURE_KEY, JSONObject.NULL); + internalConfig.sdk.user().picturePath = null; + internalConfig.sdk.user().picture = null; } else if (value instanceof String) { if (Utils.isValidURL((String) value)) { //if it is a valid URL that means the picture is online, and we want to send the link to the server @@ -135,6 +138,7 @@ void perform(JSONObject changes) throws JSONException { //if we get here then that means it is a local file path which we would send over as bytes to the server changes.put(PICTURE_PATH_KEY, value); } + internalConfig.sdk.user().picturePath = value.toString(); } else { L.e("[UserEditorImpl] Won't set user picturePath (must be String or null)"); } @@ -189,7 +193,7 @@ private void performCustomUpdate(final String key, final Object value, final JSO if (!changes.has(CUSTOM_KEY)) { changes.put(CUSTOM_KEY, new JSONObject()); } - changes.getJSONObject(CUSTOM_KEY).put(key, value); + JSONObject custom = changes.getJSONObject(CUSTOM_KEY).put(key, value); if (value == null) { custom.remove(key); } else { @@ -210,8 +214,20 @@ private Params prepareRequestParamsForUserProfile() { Params params = new Params(); final JSONObject json = new JSONObject(); perform(json); - params.add("user_details", json.toString()); - return params; + if (json.has(PICTURE_PATH_KEY)) { + try { + params.add(PICTURE_PATH_KEY, json.getString(PICTURE_PATH_KEY)); + json.remove(PICTURE_PATH_KEY); + } catch (JSONException e) { + L.w("Won't send picturePath" + e); + } + } + if (!json.isEmpty() || internalConfig.sdk.user().picturePath != null || internalConfig.sdk.user().picture != null) { + params.add("user_details", json.toString()); + return params; + } else { + return null; + } } /** @@ -248,6 +264,10 @@ protected void saveInternal() { return; } Params generatedParams = prepareRequestParamsForUserProfile(); + if (generatedParams == null) { + L.d("[ModuleUserProfile] saveInternal, nothing to save returning"); + return; + } L.d("[ModuleUserProfile] saveInternal, generated params [" + generatedParams + "]"); ModuleRequests.pushAsync(internalConfig, new Request(generatedParams)); clearInternal(); @@ -307,7 +327,7 @@ public void incrementBy(String key, int value) { * @param key String with property name to multiply * @param value int value by which to multiply */ - public void multiply(String key, int value) { + public void multiply(String key, double value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.MUL); } @@ -319,7 +339,7 @@ public void multiply(String key, int value) { * @param key String with property name to check for max * @param value int value to check for max */ - public void saveMax(String key, int value) { + public void saveMax(String key, double value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.MAX); } @@ -331,7 +351,7 @@ public void saveMax(String key, int value) { * @param key String with property name to check for min * @param value int value to check for min */ - public void saveMin(String key, int value) { + public void saveMin(String key, double value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.MIN); } @@ -343,40 +363,46 @@ public void saveMin(String key, int value) { * @param key String with property name to set * @param value String value to set */ - public void setOnce(String key, String value) { + public void setOnce(String key, Object value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.SET_ONCE); } } - /* Create array property, if property does not exist and add value to array + /** + * Create array property, if property does not exist and add value to array * You can only use it on array properties or properties that do not exist yet + * * @param key String with property name for array property * @param value String with value to add to array */ - public void push(String key, String value) { + public void push(String key, Object value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.PUSH); } } - /* Create array property, if property does not exist and add value to array, only if value is not yet in the array + /** + * Create array property, if property does not exist and add value to array, only if value is not yet in the array * You can only use it on array properties or properties that do not exist yet + * * @param key String with property name for array property * @param value String with value to add to array */ - public void pushUnique(String key, String value) { + public void pushUnique(String key, Object value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.PUSH_UNIQUE); } } - /* Create array property, if property does not exist and remove value from array + /** + * Create array property, if property does not exist and remove value from array * You can only use it on array properties or properties that do not exist yet + * * @param key String with property name for array property * @param value String with value to remove from array */ - public void pull(String key, String value) { + public void pull(String key, Object value) { synchronized (Countly.instance()) { modifyCustomData(key, value, Op.PULL); } @@ -392,7 +418,7 @@ public void setProperty(String key, Object value) { synchronized (Countly.instance()) { L.i("[UserProfile] Calling 'setProperty'"); - Map data = new ConcurrentHashMap<>(); + Map data = new HashMap<>(); // keys should be nullable data.put(key, value); setPropertiesInternal(data); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java index a805b9e3e..6802125cf 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java @@ -269,6 +269,7 @@ protected void buildModules(InternalConfig config, int features) throws IllegalA modules.put(-3, new ModuleDeviceIdCore()); modules.put(-2, new ModuleRequests()); modules.put(CoreFeature.Sessions.getIndex(), new ModuleSessions()); + modules.put(CoreFeature.UserProfiles.getIndex(), new ModuleUserProfile()); if (config.requiresConsent()) { consents = 0; @@ -401,12 +402,6 @@ public ModuleRemoteConfig.RemoteConfig remoteConfig() { } public ModuleUserProfile.UserProfile userProfile() { - //todo is this needed? - //if (!hasConsentForFeature(CoreFeature.UserProfiles)) { - // L.v("[SDKCore] remoteConfig, RemoteConfig feature has no consent, returning null"); - // return null; - //} - return module(ModuleUserProfile.class).userProfileInterface; } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java index e0a4dd9f6..fab5053c3 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/UserEditorImpl.java @@ -175,7 +175,7 @@ public UserEditor inc(String key, int by) { @Override public UserEditor mul(String key, double by) { L.d("mul: key " + key + " by " + by); - Countly.instance().userProfile().multiply(key, Double.valueOf(by).intValue()); + Countly.instance().userProfile().multiply(key, by); return this; } @@ -189,14 +189,14 @@ public UserEditor mul(String key, double by) { @Override public UserEditor min(String key, double value) { L.d("min: key " + key + " value " + value); - Countly.instance().userProfile().saveMin(key, Double.valueOf(value).intValue()); + Countly.instance().userProfile().saveMin(key, value); return this; } @Override public UserEditor max(String key, double value) { L.d("max: key " + key + " value " + value); - Countly.instance().userProfile().saveMax(key, Double.valueOf(value).intValue()); + Countly.instance().userProfile().saveMax(key, value); return this; } @@ -207,7 +207,7 @@ public UserEditor setOnce(String key, Object value) { L.e("[UserEditorImpl] $setOnce operation operand cannot be null: key " + key); return this; } else { - Countly.instance().userProfile().setOnce(key, value.toString()); + Countly.instance().userProfile().setOnce(key, value); return this; } } @@ -219,7 +219,7 @@ public UserEditor pull(String key, Object value) { L.e("[UserEditorImpl] $pull operation operand cannot be null: key " + key); return this; } else { - Countly.instance().userProfile().pull(key, value.toString()); + Countly.instance().userProfile().pull(key, value); return this; } } @@ -231,7 +231,7 @@ public UserEditor push(String key, Object value) { L.e("[UserEditorImpl] $push operation operand cannot be null: key " + key); return this; } else { - Countly.instance().userProfile().push(key, value.toString()); + Countly.instance().userProfile().push(key, value); return this; } } @@ -243,7 +243,7 @@ public UserEditor pushUnique(String key, Object value) { L.e("[UserEditorImpl] pushUnique / $addToSet operation operand cannot be null: key " + key); return this; } else { - Countly.instance().userProfile().pushUnique(key, value.toString()); + Countly.instance().userProfile().pushUnique(key, value); return this; } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java index 1ba4eb0a5..fdcd2f0aa 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java @@ -111,8 +111,7 @@ public void setPicture_binaryData() { sessionHandler(() -> Countly.instance().user().edit().setPicture(imgData).commit()); validatePictureAndPath(null, imgData); Countly.session().end(); - //validatePictureInRQ("{}", UserEditorImpl.PICTURE_IN_USER_PROFILE); - validateUserDetailsRequestInRQ(map("user_details", "{}", "picturePath", UserEditorImpl.PICTURE_IN_USER_PROFILE)); + validateUserDetailsRequestInRQ(map("user_details", "{}", "picturePath", ModuleUserProfile.PICTURE_IN_USER_PROFILE)); } /** @@ -142,7 +141,7 @@ public void setOnce() { .setOnce(TestUtils.eKeys[0], 56) .setOnce(TestUtils.eKeys[0], TestUtils.eKeys[1]) .commit()); - validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], UserEditorImpl.Op.SET_ONCE, TestUtils.eKeys[1])))); + validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", TestUtils.eKeys[1])))); } /** @@ -166,7 +165,7 @@ public void setOnce_null() { public void setOnce_empty() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit().setOnce(TestUtils.eKeys[0], "").commit()); - validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], UserEditorImpl.Op.SET_ONCE, "")))); + validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", "")))); } /** @@ -174,7 +173,7 @@ public void setOnce_empty() { * Validating that all the methods are working properly * Request should contain all the parameters directly also in "user_details" json and body */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocationBasics() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit() @@ -196,7 +195,7 @@ public void setLocationBasics() { * Validating that all the methods are working properly * Request should contain all the parameters directly also in "user_details" json and body */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocationBasics_null() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit() @@ -218,7 +217,7 @@ public void setLocationBasics_null() { * Validating that all the methods are working properly * Request should contain all the parameters directly also in "user_details" json and body */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocationBasics_noConsent() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit() @@ -239,7 +238,7 @@ public void setLocationBasics_noConsent() { @Test public void pushUnique() { Countly.instance().init(TestUtils.getBaseConfig()); - pullPush_base(UserEditorImpl.Op.PUSH_UNIQUE, Countly.instance().user().edit()::pushUnique); + pullPush_base("$addToSet", Countly.instance().user().edit()::pushUnique); } /** @@ -250,7 +249,7 @@ public void pushUnique() { @Test public void pull() { Countly.instance().init(TestUtils.getBaseConfig()); - pullPush_base(UserEditorImpl.Op.PULL, Countly.instance().user().edit()::pull); + pullPush_base("$pull", Countly.instance().user().edit()::pull); } /** @@ -261,7 +260,7 @@ public void pull() { @Test public void push() { Countly.instance().init(TestUtils.getBaseConfig()); - pullPush_base(UserEditorImpl.Op.PUSH, Countly.instance().user().edit()::push); + pullPush_base("$push", Countly.instance().user().edit()::push); } private void pullPush_base(String op, BiFunction opFunction) { @@ -328,9 +327,9 @@ public void max() { ); validateUserDetailsRequestInRQ(map("user_details", c( - opJson(TestUtils.eKeys[2], UserEditorImpl.Op.MAX, 0), - opJson(TestUtils.eKeys[1], UserEditorImpl.Op.MAX, -1), - opJson(TestUtils.eKeys[0], UserEditorImpl.Op.MAX, 128))) + opJson(TestUtils.eKeys[2], "$max", 0), + opJson(TestUtils.eKeys[1], "$max", -1), + opJson(TestUtils.eKeys[0], "$max", 128))) ); } @@ -352,9 +351,9 @@ public void min() { ); validateUserDetailsRequestInRQ(map("user_details", c( - opJson(TestUtils.eKeys[2], UserEditorImpl.Op.MIN, 0), - opJson(TestUtils.eKeys[1], UserEditorImpl.Op.MIN, -155.9), - opJson(TestUtils.eKeys[0], UserEditorImpl.Op.MIN, 122))) + opJson(TestUtils.eKeys[2], "$min", 0), + opJson(TestUtils.eKeys[1], "$min", -155.9), + opJson(TestUtils.eKeys[0], "$min", 122))) ); } @@ -376,9 +375,9 @@ public void inc() { ); validateUserDetailsRequestInRQ(map("user_details", c( - opJson(TestUtils.eKeys[2], UserEditorImpl.Op.INC, 0), - opJson(TestUtils.eKeys[1], UserEditorImpl.Op.INC, -155), - opJson(TestUtils.eKeys[0], UserEditorImpl.Op.INC, 0))) + opJson(TestUtils.eKeys[2], "$inc", 0), + opJson(TestUtils.eKeys[1], "$inc", -155), + opJson(TestUtils.eKeys[0], "$inc", 0))) ); } @@ -403,10 +402,10 @@ public void mul() { ); validateUserDetailsRequestInRQ(map("user_details", c( - opJson(TestUtils.eKeys[3], UserEditorImpl.Op.MUL, -90), - opJson(TestUtils.eKeys[2], UserEditorImpl.Op.MUL, 0), - opJson(TestUtils.eKeys[1], UserEditorImpl.Op.MUL, -5.28), - opJson(TestUtils.eKeys[0], UserEditorImpl.Op.MUL, 0))) + opJson(TestUtils.eKeys[3], "$mul", -90), + opJson(TestUtils.eKeys[2], "$mul", 0), + opJson(TestUtils.eKeys[1], "$mul", -5.28), + opJson(TestUtils.eKeys[0], "$mul", 0))) ); } @@ -433,7 +432,7 @@ public void setUserBasics() { "name", "Test", "username", "TestUsername", "email", "test@test.test", - "org", "TestOrg", + "organization", "TestOrg", "phone", "123456789", "byear", 1999, "gender", "M" @@ -463,7 +462,7 @@ public void setUserBasics_null() { "name", JSONObject.NULL, "username", JSONObject.NULL, "email", JSONObject.NULL, - "org", JSONObject.NULL, + "organization", JSONObject.NULL, "phone", JSONObject.NULL, "byear", JSONObject.NULL, "gender", JSONObject.NULL @@ -547,7 +546,7 @@ private void setGender_base(Object gender, Map expectedValues) { * Validating that values is correctly parsed to the long and added to the request, * Request should contain "location" parameter in "user_details" json and "location" parameter in the request */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocation_fromString() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation("-40.7128, 74.0060").commit()); @@ -559,7 +558,7 @@ public void setLocation_fromString() { * Validating that values is correctly parsed to the long and added to the request, * Request should contain "location" parameter in "user_details" json and "location" parameter in the request */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocation_fromString_noConsent() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit().setLocation("32.78, 28.01").commit()); @@ -595,7 +594,7 @@ public void setLocation_fromString_onePair() { * Validating that location is nullified * Request should contain "location" parameter in "user_details" json and request body and should be null */ - @Test + // @Test //todo this test will be needed rework with location module public void setLocation_fromString_null() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation(null).commit()); @@ -607,7 +606,7 @@ public void setLocation_fromString_null() { * Validating that calling the function will result in nullifying the location relates params * Request should contain "location","country_code","city" parameters in the body and should be null */ - @Test + // @Test //todo this test will be needed rework with location module public void optOutFromLocationServices() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().optOutFromLocationServices().commit()); @@ -623,23 +622,23 @@ public void optOutFromLocationServices() { public void set_notAString() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit() - .set(UserEditorImpl.NAME, new TestUtils.AtomicString("Magical")) - .set(UserEditorImpl.USERNAME, new TestUtils.AtomicString("TestUsername")) - .set(UserEditorImpl.EMAIL, new TestUtils.AtomicString("test@test.ly")) - .set(UserEditorImpl.ORG, new TestUtils.AtomicString("Magical Org")) - .set(UserEditorImpl.PHONE, 123456789) - .set(UserEditorImpl.PICTURE, new TestUtils.AtomicString("Not a picture")) - .set(UserEditorImpl.PICTURE_PATH, new TestUtils.AtomicString("Not a picture path")) - .set(UserEditorImpl.BIRTHYEAR, new TestUtils.AtomicString("Not a birthyear")) - .set(UserEditorImpl.LOCATION, new TestUtils.AtomicString("Not a location")) - .set(UserEditorImpl.CITY, new TestUtils.AtomicString("Not a city")) - .set(UserEditorImpl.COUNTRY, new TestUtils.AtomicString("Not a country")) - .set(UserEditorImpl.LOCALE, new TestUtils.AtomicString("Not a locale")) + .set(ModuleUserProfile.NAME_KEY, new TestUtils.AtomicString("Magical")) + .set(ModuleUserProfile.USERNAME_KEY, new TestUtils.AtomicString("TestUsername")) + .set(ModuleUserProfile.EMAIL_KEY, new TestUtils.AtomicString("test@test.ly")) + .set(ModuleUserProfile.ORG_KEY, new TestUtils.AtomicString("Magical Org")) + .set(ModuleUserProfile.PHONE_KEY, 123456789) + .set(ModuleUserProfile.PICTURE_KEY, new TestUtils.AtomicString("Not a picture")) + .set(ModuleUserProfile.PICTURE_PATH_KEY, new TestUtils.AtomicString("Not a picture path")) + .set(ModuleUserProfile.BYEAR_KEY, new TestUtils.AtomicString("Not a birthyear")) + //.set(ModuleUserProfile.LOCATION_KEY, new TestUtils.AtomicString("Not a location")) + //.set(ModuleUserProfile.CITY_KEY, new TestUtils.AtomicString("Not a city")) + //.set(ModuleUserProfile.COUNTRY_KEY, new TestUtils.AtomicString("Not a country")) + //.set(ModuleUserProfile.LOCALE_KEY, new TestUtils.AtomicString("Not a locale")) .commit()); validateUserDetailsRequestInRQ(map("user_details", json("name", "Magical", "username", "TestUsername", "email", "test@test.ly", - "org", "Magical Org", + "organization", "Magical Org", "phone", "123456789")) ); } @@ -648,7 +647,7 @@ public void set_notAString() { * Set various kind of user properties and validate that they are added to the request * There should be 2 request, and it should be a session begin and end. End request should contain all the properties */ - @Test + // @Test //todo this test will be needed rework with location module public void set_multipleCalls_sessionsEnabled() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Sessions, Config.Feature.Location).setUpdateSessionTimerDelay(1)); sessionHandler(() -> Countly.instance().user().edit() @@ -670,7 +669,7 @@ public void set_multipleCalls_sessionsEnabled() { "gender", "F", "picture", "https://someurl.com", "email", "SomeEmail", - "custom", jsonObj(map(TestUtils.eKeys[0], map(UserEditorImpl.Op.PUSH, new Object[] { 56, "TW" }), "some_custom", 56))), + "custom", jsonObj(map(TestUtils.eKeys[0], map(ModuleUserProfile.Op.PUSH, new Object[] { 56, "TW" }), "some_custom", 56))), "country_code", "US", "city", "New York", "location", "40.7128,-74.006", From 4cd94e87a35411d4472bc37c5e8234ce8959c7da Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 20 Nov 2023 12:13:16 +0300 Subject: [PATCH 5/5] feat: tests for the new calls --- .../sdk/java/internal/ModuleUserProfile.java | 8 +- .../java/internal/ModuleUserProfileTests.java | 217 ++++++++++++++++++ .../sdk/java/internal/UserEditorTests.java | 18 +- 3 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java index e014e968d..2896a5793 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java @@ -232,12 +232,17 @@ private Params prepareRequestParamsForUserProfile() { /** * Atomic modifications on custom user property. + * If value null, call will be ignored * * @param key String with property name to modify * @param value String value to use in modification * @param mod String with modification command */ - protected void modifyCustomData(String key, Object value, Op mod) { + private void modifyCustomData(String key, Object value, Op mod) { + if (value == null) { + L.w("[ModuleUserProfile] modifyCustomData, value is null, thus nothing to modify"); + return; + } ops.add(new OpParams(key, value, mod)); isSynced = false; } @@ -298,6 +303,7 @@ public void stop(InternalConfig config, boolean clearData) { } public class UserProfile { + /** * Increment custom property value by 1. * diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java new file mode 100644 index 000000000..829497209 --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java @@ -0,0 +1,217 @@ +package ly.count.sdk.java.internal; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import ly.count.sdk.java.Countly; +import ly.count.sdk.java.User; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ModuleUserProfileTests { + @Before + public void beforeTest() { + TestUtils.createCleanTestState(); + } + + @After + public void stop() { + Countly.instance().halt(); + } + + /** + * "setProperties", "setProperty" with user basics + * Validating new calls to "setProperties" and "setProperty" with user basics + * Values should be under "user_details" key and request must generate + */ + @Test + public void setUserBasics() { + Countly.instance().init(TestUtils.getBaseConfig()); + + Map basics = new ConcurrentHashMap<>(); + basics.put("name", "Test"); + basics.put("username", "TestUsername"); + basics.put("email", "test@test.test"); + basics.put("organization", "TestOrg"); + basics.put("phone", "123456789"); + + Countly.instance().userProfile().setProperties(basics); + Countly.instance().userProfile().setProperty("byear", 1999); + Countly.instance().userProfile().setProperty("gender", User.Gender.MALE); + + Countly.instance().userProfile().save(); + + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.json( + "name", "Test", + "username", "TestUsername", + "email", "test@test.test", + "organization", "TestOrg", + "phone", "123456789", + "byear", 1999, + "gender", "M" + ))); + } + + /** + * "clear" + * Validating that after "clear" call, registered user details are cleared + * Values should be under "user_details" key and request must generate and only first saved props must exist + */ + @Test + public void clear() { + Countly.instance().init(TestUtils.getBaseConfig()); + Map mixed = new ConcurrentHashMap<>(); + mixed.put("name", "Test"); + mixed.put("username", "TestUsername"); + mixed.put("level", 56); + + Countly.instance().userProfile().setProperties(mixed); + Countly.instance().userProfile().save(); + mixed.clear(); + Countly.instance().userProfile().setProperty("byear", 1999); + Countly.instance().userProfile().setProperty("gender", User.Gender.MALE); + Countly.instance().userProfile().clear(); + Countly.instance().userProfile().save(); + + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.json( + "name", "Test", + "username", "TestUsername", + "custom", UserEditorTests.map("level", 56) + ))); + } + + /** + * "setProperties" with null and empty maps + * Validating that no request is generated with null and empty maps + * No request must be generated + */ + @Test + public void setProperties_empty_null() { + Countly.instance().init(TestUtils.getBaseConfig()); + Countly.instance().userProfile().setProperties(null); + Countly.instance().userProfile().setProperties(new ConcurrentHashMap<>()); + Countly.instance().userProfile().save(); + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map()); + } + + /** + * "increment", "incrementBy" + * Validating that "increment" and "incrementBy" calls are generating correct requests + * Values should be under "user_details" key and request must generate + */ + @Test + public void increment() { + Countly.instance().init(TestUtils.getBaseConfig()); + Countly.instance().userProfile().increment("test"); + Countly.instance().userProfile().incrementBy("test", 2); + Countly.instance().userProfile().save(); + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", + UserEditorTests.c(UserEditorTests.opJson("test", "$inc", 3)) + )); + } + + /** + * "saveMax", "saveMin" + * Validating that "saveMax" and "saveMin" calls are generating correct requests + * Values should be under "user_details" key and request must generate + */ + @Test + public void saveMax_Min() { + Countly.instance().init(TestUtils.getBaseConfig()); + Countly.instance().userProfile().saveMax(TestUtils.eKeys[0], 6); + Countly.instance().userProfile().saveMax(TestUtils.eKeys[0], 9.62); + + Countly.instance().userProfile().saveMin(TestUtils.eKeys[1], 2); + Countly.instance().userProfile().saveMin(TestUtils.eKeys[1], 0.002); + Countly.instance().userProfile().save(); + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.opJson(TestUtils.eKeys[1], "$min", 0.002), + UserEditorTests.opJson(TestUtils.eKeys[0], "$max", 9.62) + ))); + } + + /** + * "multiply" + * Validating that "multiply" call are generating correct requests + * Values should be under "user_details" key and request must generate + */ + @Test + public void multiply() { + Countly.instance().init(TestUtils.getBaseConfig()); + Countly.instance().userProfile().multiply("test", 2); + Countly.instance().userProfile().save(); + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", + UserEditorTests.c(UserEditorTests.opJson("test", "$mul", 2)) + )); + } + + /** + * "pushUnique" with multiple calls + * Validating that multiple calls to pushUnique with same key will result in only one key in the request + * All added values must be form an array in the request except null + */ + @Test + public void pushUnique() { + Countly.instance().init(TestUtils.getBaseConfig()); + pullPush_base("$addToSet", Countly.instance().userProfile()::pushUnique); + } + + /** + * "pull" with multiple calls + * Validating that multiple calls to pushUnique with same key will result in only one key in the request + * All added values must be form an array in the request + */ + @Test + public void pull() { + Countly.instance().init(TestUtils.getBaseConfig()); + pullPush_base("$pull", Countly.instance().userProfile()::pull); + } + + /** + * "push" with multiple calls + * Validating that multiple calls to pushUnique with same key will result in only one key in the request + * All added values must be form an array in the request + */ + @Test + public void push() { + Countly.instance().init(TestUtils.getBaseConfig()); + pullPush_base("$push", Countly.instance().userProfile()::push); + } + + public void pullPush_base(String op, BiConsumer opFunction) { + opFunction.accept(TestUtils.eKeys[0], TestUtils.eKeys[1]); + opFunction.accept(TestUtils.eKeys[0], TestUtils.eKeys[2]); + opFunction.accept(TestUtils.eKeys[0], 89); + opFunction.accept(TestUtils.eKeys[0], TestUtils.eKeys[2]); + opFunction.accept(TestUtils.eKeys[3], TestUtils.eKeys[2]); + opFunction.accept(TestUtils.eKeys[0], null); + opFunction.accept(TestUtils.eKeys[0], ""); + + Countly.instance().userProfile().save(); + + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.opJson(TestUtils.eKeys[3], op, TestUtils.eKeys[2]), + UserEditorTests.opJson(TestUtils.eKeys[0], op, TestUtils.eKeys[1], TestUtils.eKeys[2], 89, TestUtils.eKeys[2], "") + ) + )); + } + + /** + * "setOnce" with multiple calls + * Validating that multiple calls to setOnce with same key will result in only one key in the request + * Last calls' value should be the one in the request + */ + @Test + public void setOnce() { + Countly.instance().init(TestUtils.getBaseConfig()); + Countly.instance().userProfile().setOnce(TestUtils.eKeys[0], 56); + Countly.instance().userProfile().setOnce(TestUtils.eKeys[0], TestUtils.eKeys[1]); + Countly.instance().userProfile().save(); + UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.opJson(TestUtils.eKeys[0], "$setOnce", TestUtils.eKeys[1])))); + } +} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java index fdcd2f0aa..c803a61f6 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java @@ -682,7 +682,7 @@ private void validatePictureAndPath(String picturePath, byte[] picture) { Assert.assertEquals(picture, Countly.instance().user().picture()); } - private void validateUserDetailsRequestInRQ(Map expectedParams) { + protected static void validateUserDetailsRequestInRQ(Map expectedParams) { validateUserDetailsRequestInRQ(expectedParams, 0); } @@ -693,7 +693,7 @@ private void validateUserDetailsRequestInRQ(Map expectedParams) * @param expectedParams expected parameters in the request * @param requestIndex index of the request in the request queue */ - private void validateUserDetailsRequestInRQ(Map expectedParams, final int requestIndex) { + protected static void validateUserDetailsRequestInRQ(Map expectedParams, final int requestIndex) { if (expectedParams.isEmpty()) { // nothing to validate, just return Assert.assertEquals(0, TestUtils.getCurrentRQ().length); return; @@ -721,7 +721,7 @@ private void validateUserDetailsRequestInRQ(Map expectedParams, * * @param request request to validate */ - private void validateBeginSession(Map request) { + protected static void validateBeginSession(Map request) { TestUtils.validateRequiredParams(request); TestUtils.validateMetrics(request.get("metrics")); Assert.assertEquals("1", request.get("begin_session")); @@ -735,7 +735,7 @@ private void validateBeginSession(Map request) { * @param entries json entries * @return wrapped json */ - private String c(String... entries) { + protected static String c(String... entries) { return "{\"custom\":{" + String.join(",", entries) + "}}"; } @@ -757,7 +757,7 @@ private String c(Map entries) { * @param values values * @return json string */ - private String opJson(String key, String op, Object... values) { + protected static String opJson(String key, String op, Object... values) { JSONObject obj = new JSONObject(); if (values.length == 1) { obj.put(op, values[0]); @@ -786,7 +786,7 @@ private void sessionHandler(Supplier process) { * @param entries map to convert * @return json string */ - private String json(Map entries) { + protected static String json(Map entries) { return jsonObj(entries).toString(); } @@ -796,7 +796,7 @@ private String json(Map entries) { * @param entries map to convert * @return json string */ - private JSONObject jsonObj(Map entries) { + protected static JSONObject jsonObj(Map entries) { JSONObject json = new JSONObject(); entries.forEach(json::put); return json; @@ -809,7 +809,7 @@ private JSONObject jsonObj(Map entries) { * @param args array of objects * @return json string */ - private String json(Object... args) { + protected static String json(Object... args) { if (args == null || args.length == 0) { return "{}"; } @@ -822,7 +822,7 @@ private String json(Object... args) { * @param args array of objects * @return map */ - private Map map(Object... args) { + protected static Map map(Object... args) { Map map = new ConcurrentHashMap<>(); if (args.length % 2 == 0) { for (int i = 0; i < args.length; i += 2) {