Skip to content

Commit

Permalink
[Java] Adding profile picture need rework (#128)
Browse files Browse the repository at this point in the history
* fix: picture sending

* feat: url setting

* feat: changelog for it

* Adding comments

* commenting

* revert: url params way

* fix: picture setting

* fix: add missing assignment

* feat: user editor pic related tests

* feat: picture path validations

* some twaeks

* some additional changes

* Update CHANGELOG.md

* fix: nullify picture not picture path

* fix: user profile tests

* fix: mock image data with byte directly

* fix: rename multipart param name

* fix: missing req params

* fix: check for param existing

* Update CHANGELOG.md

---------

Co-authored-by: ArtursK <[email protected]>
  • Loading branch information
arifBurakDemiray and ArtursKadikis authored Oct 31, 2023
1 parent befedc2 commit d1447ec
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 63 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* Session update time duration increased to 60 seconds from 30 seconds.
* Adding remaining request queue size information to every request.
* Adding application version information to every request.
* Added the ability to set the user profile picture with an URL

* Fixed a bug where it was not possible to send a profile picture with binary data

## 23.8.0

Expand Down
4 changes: 1 addition & 3 deletions sdk-java/src/main/java/ly/count/sdk/java/User.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package ly.count.sdk.java;

import ly.count.sdk.java.internal.Device;

import java.util.Map;
import java.util.Set;
import ly.count.sdk.java.internal.Device;

//import ly.count.sdk.android.internal.Device;

Expand Down
98 changes: 59 additions & 39 deletions sdk-java/src/main/java/ly/count/sdk/java/internal/Transport.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -128,11 +126,15 @@ HttpURLConnection connection(final Request request, final User user) throws IOEx
}

String path = config.getServerURL().toString() + endpoint;
String picture = request.params.remove(UserEditorImpl.PICTURE_PATH);
boolean usingGET = !config.isHTTPPostForced() && request.isGettable(config.getServerURL()) && Utils.isEmptyOrNull(picture);
String picturePathValue = request.params.remove(UserEditorImpl.PICTURE_PATH);
boolean usingGET = !config.isHTTPPostForced() && request.isGettable(config.getServerURL()) && Utils.isEmptyOrNull(picturePathValue);

if (!usingGET && !Utils.isEmptyOrNull(picturePathValue)) {
path = setProfilePicturePathRequestParams(path, request.params);
}

if (usingGET && config.getParameterTamperingProtectionSalt() != null) {
request.params.add(CHECKSUM, Utils.digestHex(PARAMETER_TAMPERING_DIGEST, request.params.toString() + config.getParameterTamperingProtectionSalt(), L));
request.params.add(CHECKSUM, Utils.digestHex(PARAMETER_TAMPERING_DIGEST, request.params + config.getParameterTamperingProtectionSalt(), L));
}

HttpURLConnection connection = openConnection(path, request.params.toString(), usingGET);
Expand All @@ -148,18 +150,18 @@ HttpURLConnection connection(final Request request, final User user) throws IOEx
OutputStream output = null;
PrintWriter writer = null;
try {
L.d("[network] Picture " + picture);
byte[] data = picture == null ? null : pictureData(user, picture);
L.d("[network] Picture path value " + picturePathValue);
byte[] pictureByteData = picturePathValue == null ? null : getPictureDataFromGivenValue(user, picturePathValue);

if (data != null) {
if (pictureByteData != null) {
String boundary = Long.toHexString(System.currentTimeMillis());

connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

output = connection.getOutputStream();
writer = new PrintWriter(new OutputStreamWriter(output, Utils.UTF8), true);

addMultipart(output, writer, boundary, "", "profilePicture", "image", data);
addMultipart(output, writer, boundary, "image/jpeg", "binaryFile", "image", pictureByteData);

StringBuilder salting = new StringBuilder();
Map<String, String> map = request.params.map();
Expand All @@ -175,6 +177,8 @@ HttpURLConnection connection(final Request request, final User user) throws IOEx

writer.append(Utils.CRLF).append("--").append(boundary).append("--").append(Utils.CRLF).flush();
} else {
//picture data is "null". If it was sent, we send "null" to server to clear the image there
//we send a normal request in HTTP POST
if (config.getParameterTamperingProtectionSalt() != null) {
request.params.add(CHECKSUM, Utils.digestHex(PARAMETER_TAMPERING_DIGEST, request.params.toString() + config.getParameterTamperingProtectionSalt(), L));
}
Expand Down Expand Up @@ -222,43 +226,35 @@ void addMultipart(OutputStream output, PrintWriter writer, String boundary, Stri
}
}

byte[] pictureData(User user, String picture) throws IOException {
if (user == null) return null;
byte[] data;
/**
* Returns valid picture information
* If we have the bytes, give them
* Otherwise load them from disk
*
* @param user
* @param picture
* @return
*/
byte[] getPictureDataFromGivenValue(User user, String picture) {
if (user == null) {
return null;
}

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 {
String protocol = null;
//otherwise we assume it is a local path, and we try to read it from disk
try {
URI uri = new URI(picture);
protocol = uri.isAbsolute() ? uri.getScheme() : new URL(picture).getProtocol();
} catch (Throwable t) {
try {
File f = new File(picture);
protocol = f.toURI().toURL().getProtocol();
} catch (Throwable tt) {
L.w("[network] Couldn't determine picturePath protocol " + tt);
}
}
if (protocol != null && protocol.equals("file")) {
File file = new File(picture);
if (!file.exists()) {
return null;
}
FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}

input.close();
data = output.toByteArray();
output.close();
} else {
return null;
data = Files.readAllBytes(file.toPath());
} catch (Throwable t) {
L.w("[Transport] getPictureDataFromGivenValue, Error while reading picture from disk " + t);
}
}

Expand Down Expand Up @@ -506,4 +502,28 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) throws
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}

private String setProfilePicturePathRequestParams(String path, Params params) {
Params tempParams = new Params();

tempParams.add("device_id", params.get("device_id"));
tempParams.add("app_key", params.get("app_key"));
tempParams.add("timestamp", params.get("timestamp"));
tempParams.add("sdk_name", params.get("sdk_name"));
tempParams.add("sdk_version", params.get("sdk_version"));
tempParams.add("tz", params.get("tz"));
tempParams.add("hour", params.get("hour"));
tempParams.add("dow", params.get("dow"));
tempParams.add("rr", params.get("rr"));

if (params.has("av")) {
tempParams.add("av", params.get("av"));
}
//if no user details, add empty user details to indicate that we are sending a picture
if (!params.has("user_details")) {
tempParams.add("user_details", "{}");
}

return path + tempParams;
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
package ly.count.sdk.java.internal;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URI;
import java.net.URISyntaxException;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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;
Expand Down Expand Up @@ -119,6 +113,12 @@ public void apply(JSONObject json) throws JSONException {
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);
Expand Down Expand Up @@ -169,29 +169,35 @@ void perform(JSONObject changes) throws JSONException {
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_PATH, JSONObject.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) {
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_PATH, JSONObject.NULL);
changes.put(PICTURE, JSONObject.NULL);
} else if (value instanceof String) {
try {
user.picturePath = new URI((String) value).toString();
changes.put(PICTURE_PATH, user.picturePath);
} catch (URISyntaxException e) {
L.e("[UserEditorImpl] Supplied picturePath is not parsable to java.net.URI");
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)");
}
Expand Down Expand Up @@ -331,17 +337,23 @@ public UserEditor setPhone(String value) {
return set(PHONE, value);
}

//we set the bytes for the local picture
@Override
public UserEditor setPicture(byte[] picture) {
L.d("setPicture: picture = " + picture);
return set(PICTURE, picture);
}

//we set the url for either the online picture or a local path picture
@Override
public UserEditor setPicturePath(String picturePath) {
L.d("setPicturePath: picturePath = " + picturePath);

return set(PICTURE_PATH, 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);
}
L.w("[UserEditorImpl] setPicturePath, picturePath is not a valid file path or url");
return this;
}

@Override
Expand Down Expand Up @@ -526,10 +538,10 @@ public User commit() {
Storage.push(SDKCore.instance.config, user);

ModuleRequests.injectParams(SDKCore.instance.config, params -> {
params.add("user_details", changes.toString());
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);
}
Expand All @@ -546,6 +558,7 @@ public User commit() {
if (changes.has(LOCATION) && user.location != null) {
params.add("location", user.location);
}
params.add("user_details", changes.toString());
});
} catch (JSONException e) {
L.e("[UserEditorImpl] Exception while committing changes to User profile" + e);
Expand Down
14 changes: 14 additions & 0 deletions sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,18 @@ public static String decodeToString(String string, Log L) {
}
}
}

/**
* Check whether given string is a valid URL or not
* @param url
* @return
*/
public static boolean isValidURL(String url) {
try {
new java.net.URL(url).toURI();
return true;
} catch (Exception e) {
return false;
}
}
}
21 changes: 21 additions & 0 deletions sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static Config getBaseConfig(String deviceID) {
checkSdkStorageRootDirectoryExist(sdkStorageRootDirectory);
Config config = new Config(SERVER_URL, SERVER_APP_KEY, sdkStorageRootDirectory);

config.setApplicationVersion(APPLICATION_VERSION);
config.setCustomDeviceId(deviceID);
return config;
}
Expand Down Expand Up @@ -412,6 +413,26 @@ static JSONObject readJsonFile(final File file) {
}
}

/**
* Create a file for test purposes
* If file cannot be created, return null and assert fail
*
* @param fileName name of the file to create
* @return created file
*/
public static File createFile(final String fileName) {
File file = new File(getTestSDirectory(), FILE_NAME_PREFIX + FILE_NAME_SEPARATOR + fileName);
try {
if (file.createNewFile()) {
return file;
}
} catch (IOException e) {
Assert.fail("Failed to create file: " + e.getMessage());
}

return file;
}

static InternalConfig getInternalConfigWithLogger(Config config) {
InternalConfig ic = new InternalConfig(config);
ic.setLogger(mock(Log.class));
Expand Down
Loading

0 comments on commit d1447ec

Please sign in to comment.