diff --git a/plugins/library/src/main/java/com/deploygate/plugins/BaseSdkPlugin.kt b/plugins/library/src/main/java/com/deploygate/plugins/BaseSdkPlugin.kt index 2e072ea..fe1b960 100644 --- a/plugins/library/src/main/java/com/deploygate/plugins/BaseSdkPlugin.kt +++ b/plugins/library/src/main/java/com/deploygate/plugins/BaseSdkPlugin.kt @@ -16,7 +16,7 @@ abstract class BaseSdkPlugin : Plugin { /** * sdk/java/com/deploygate/sdk/HostAppTest.java needs to be changed for a new release */ - const val ARTIFACT_VERSION = "4.7.1" + const val ARTIFACT_VERSION = "4.8.0" val JAVA_VERSION = JavaVersion.VERSION_1_7 } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 938dfec..f8cda70 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -15,6 +15,9 @@ + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/deploygate/sample/App.java b/sample/src/main/java/com/deploygate/sample/App.java index cce2f7f..6bc710c 100644 --- a/sample/src/main/java/com/deploygate/sample/App.java +++ b/sample/src/main/java/com/deploygate/sample/App.java @@ -1,9 +1,11 @@ package com.deploygate.sample; import android.app.Application; +import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; +import com.deploygate.sdk.CustomAttributes; import com.deploygate.sdk.DeployGate; import com.deploygate.sdk.DeployGateCallback; @@ -34,6 +36,10 @@ public void onInitialized(boolean isServiceAvailable) { if (isServiceAvailable) { Log.i(TAG, "SDK is available"); DeployGate.logInfo("SDK is available"); + + CustomAttributes attrs = DeployGate.getBuildEnvironment(); + attrs.putString("build_type", BuildConfig.BUILD_TYPE); + attrs.putString("flavor", BuildConfig.FLAVOR); } else { Log.i(TAG, "SDK is unavailable"); DeployGate.logInfo("SDK is unavailable"); // this fails silently @@ -84,4 +90,14 @@ public void onUpdateAvailable( // // You can use DeployGate.isAuthorized() later to check the installation is valid or not. } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + CustomAttributes attrs = DeployGate.getRuntimeExtra(); + attrs.putString("locale", newConfig.locale.toString()); + attrs.putInt("orientation", newConfig.orientation); + attrs.putFloat("font_scale", newConfig.fontScale); + } } diff --git a/sample/src/main/java/com/deploygate/sample/SampleActivity.java b/sample/src/main/java/com/deploygate/sample/SampleActivity.java index 5e69045..c8894ca 100644 --- a/sample/src/main/java/com/deploygate/sample/SampleActivity.java +++ b/sample/src/main/java/com/deploygate/sample/SampleActivity.java @@ -14,6 +14,7 @@ import android.widget.TextView; import android.widget.Toast; +import com.deploygate.sdk.CustomAttributes; import com.deploygate.sdk.DeployGate; import com.deploygate.sdk.DeployGateCallback; @@ -53,6 +54,15 @@ public void onCreate(Bundle savedInstanceState) { mUpdateButton = (Button) findViewById(R.id.updateButton); mLogMessage = (EditText) findViewById(R.id.message); mDistributionComments = (LinearLayout) findViewById(R.id.distributionComments); + + + CustomAttributes attrs = DeployGate.getRuntimeExtra(); + attrs.putString("string", "value"); + attrs.putInt("int", 123); + attrs.putBoolean("boolean", true); + attrs.putFloat("float", 1.23f); + attrs.putDouble("double", 1.23); + attrs.putLong("long", 123L); } @Override diff --git a/sdk/src/main/java/com/deploygate/sdk/CustomAttributes.java b/sdk/src/main/java/com/deploygate/sdk/CustomAttributes.java new file mode 100644 index 0000000..d35e7b9 --- /dev/null +++ b/sdk/src/main/java/com/deploygate/sdk/CustomAttributes.java @@ -0,0 +1,193 @@ +package com.deploygate.sdk; + +import com.deploygate.sdk.internal.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.regex.Pattern; + +/** + * This class provides store key-value pairs. + * These methods are thread-safe. + */ +public final class CustomAttributes { + + private static final String TAG = "CustomAttributes"; + + private static final int MAX_ATTRIBUTES_SIZE = 8; + private static final Pattern VALID_KEY_PATTERN = Pattern.compile("^[a-z][_a-z0-9]{2,31}$"); + private static final int MAX_VALUE_LENGTH = 64; + + private final Object mLock; + private JSONObject attributes; + + CustomAttributes() { + mLock = new Object(); + attributes = new JSONObject(); + } + + /** + * Put a string value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value value must be non-null and its length must be less than 64. + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putString(String key, String value) { + return putInternal(key, value); + } + + /** + * Put an int value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value int value + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putInt(String key, int value) { + return putInternal(key, value); + } + + /** + * Put a long value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value long value + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putLong(String key, long value) { + return putInternal(key, value); + } + + /** + * Put a float value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value float value + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putFloat(String key, float value) { + return putInternal(key, value); + } + + /** + * Put a double value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value double value + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putDouble(String key, double value) { + return putInternal(key, value); + } + + /** + * Put a boolean value with the key. + * If the key already exists, the value will be overwritten. + * @param key key must be non-null and match the valid pattern. + * @param value boolean value + * @return true if the value is put successfully, otherwise false. + * @see CustomAttributes#VALID_KEY_PATTERN + */ + public boolean putBoolean(String key, boolean value) { + return putInternal(key, value); + } + + /** + * Remove the value with the key. + * @param key name of the key to be removed. + */ + public void remove(String key) { + synchronized (mLock) { + attributes.remove(key); + } + } + + /** + * Remove all key-value pairs. + */ + public void removeAll() { + synchronized (mLock) { + // recreate new object instead of removing all keys + attributes = new JSONObject(); + } + } + + int size() { + synchronized (mLock) { + return attributes.length(); + } + } + + String getJSONString() { + synchronized (mLock) { + return attributes.toString(); + } + } + + private boolean putInternal(String key, Object value) { + if (!isValidKey(key)) { + return false; + } + + if (!isValidValue(value)) { + return false; + } + + synchronized (mLock) { + try { + attributes.put(key, value); + + if (attributes.length() > MAX_ATTRIBUTES_SIZE) { + // rollback put operation + attributes.remove(key); + Logger.w(TAG, "Attributes already reached max size. Ignored: " + key); + return false; + } + } catch (JSONException e) { + Logger.w(TAG, "Failed to put attribute: " + key, e); + return false; + } + } + + return true; + } + + private boolean isValidKey(String key) { + if (key == null || key.equals("true") || key.equals("false") || key.equals("null")) { + Logger.w(TAG, "Not allowed key: " + key); + return false; + } + + if (!VALID_KEY_PATTERN.matcher(key).matches()) { + Logger.w(TAG, "Invalid key: " + key); + return false; + } + + return true; + } + + private boolean isValidValue(Object value) { + if (value == null) { + Logger.w(TAG, "Value is null"); + return false; + } + + if (value instanceof String && ((String) value).length() > MAX_VALUE_LENGTH) { + Logger.w(TAG, "Value too long: " + value); + return false; + } else if (value instanceof String || value instanceof Number || value instanceof Boolean) { + return true; + } else { + // dead code + Logger.w(TAG, "Invalid value: " + value); + return false; + } + } +} diff --git a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java index a9e60a7..cdb35bf 100644 --- a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java +++ b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java @@ -3,10 +3,13 @@ import android.app.Application; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -21,6 +24,8 @@ import com.deploygate.service.IDeployGateSdkService; import com.deploygate.service.IDeployGateSdkServiceCallback; +import org.json.JSONObject; + import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -49,6 +54,11 @@ public class DeployGate { private static final String DEPLOYGATE_PACKAGE = "com.deploygate"; private static final Object sPendingEventLock = new Object(); + private static final Object sLock = new Object(); + private static CustomAttributes sBuildEnvironment; + private static CustomAttributes sRuntimeExtra; + private static CustomAttributes sSdkDeviceStates; + private static DeployGate sInstance; private final Context mApplicationContext; @@ -127,6 +137,35 @@ public void onEvent( } else { Logger.w("streamed logcat is not supported"); } + } else if (DeployGateEvent.ACTION_COLLECT_DEVICE_STATES.equals(action)) { + Uri targetUri = Uri.parse(extras.getString(DeployGateEvent.EXTRA_TARGET_URI_FOR_REPORT_DEVICE_STATES)); + Logger.d("collect-device-status event received: %s", targetUri); + + ContentValues cv = new ContentValues(); + + String buildEnvironmentJSON = getBuildEnvironment().getJSONString(); + if (!buildEnvironmentJSON.equals("{}")) { + cv.put(DeployGateEvent.ATTRIBUTE_KEY_BUILD_ENVIRONMENT, buildEnvironmentJSON); + } + + String runtimeExtraJSON = getRuntimeExtra().getJSONString(); + if (!runtimeExtraJSON.equals("{}")) { + cv.put(DeployGateEvent.ATTRIBUTE_KEY_RUNTIME_EXTRAS, runtimeExtraJSON); + } + + String sdkDeviceStatusJSON = getSdkDeviceStates().getJSONString(); + if (!sdkDeviceStatusJSON.equals("{}")) { + cv.put(DeployGateEvent.ATTRIBUTE_KEY_SDK_DEVICE_STATES, sdkDeviceStatusJSON); + } + + cv.put(DeployGateEvent.ATTRIBUTE_KEY_EVENT_AT, System.currentTimeMillis()); + + try { + ContentResolver cr = mApplicationContext.getContentResolver(); + cr.insert(targetUri, cv); + } catch (Throwable t) { + Logger.w(t, "failed to report device states"); + } } else { Logger.w("%s is not supported by this sdk version", action); } @@ -1390,4 +1429,49 @@ public static String getDistributionUserName() { return sInstance.mDistributionUserName; } + + public static CustomAttributes getBuildEnvironment() { + if (sBuildEnvironment != null) { + return sBuildEnvironment; + } + + synchronized (sLock) { + if (sBuildEnvironment != null) { + return sBuildEnvironment; + } + sBuildEnvironment = new CustomAttributes(); + } + + return sBuildEnvironment; + } + + public static CustomAttributes getRuntimeExtra() { + if (sRuntimeExtra != null) { + return sRuntimeExtra; + } + + synchronized (sLock) { + if (sRuntimeExtra != null) { + return sRuntimeExtra; + } + sRuntimeExtra = new CustomAttributes(); + } + + return sRuntimeExtra; + } + + private static CustomAttributes getSdkDeviceStates() { + if (sSdkDeviceStates != null) { + return sSdkDeviceStates; + } + + synchronized (sLock) { + if (sSdkDeviceStates != null) { + return sSdkDeviceStates; + } + sSdkDeviceStates = new CustomAttributes(); + } + + return sSdkDeviceStates; + } } diff --git a/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java b/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java index 8b12a57..ad77118 100644 --- a/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java +++ b/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java @@ -8,6 +8,7 @@ public interface DeployGateEvent { // namespace: // ACTION => a // EXTRA => e + // ATTRIBUTE_KEY => ak // // content: // should be hyphen-separated string and be lower cases @@ -27,6 +28,11 @@ public interface DeployGateEvent { public static final String ACTION_COMPOSE_COMMENT = "composeComment"; public static final String ACTION_VISIBILITY_EVENT = "a.visibility-event"; + /** + * @since 4.8.0 + */ + public static final String ACTION_COLLECT_DEVICE_STATES = "a.collect-device-states"; + public static final String EXTRA_AUTHOR = "author"; public static final String EXTRA_EXPECTED_AUTHOR = "expectedAuthor"; @@ -121,6 +127,31 @@ public interface DeployGateEvent { */ public static final String EXTRA_VISIBILITY_EVENT_ELAPSED_REAL_TIME_IN_NANOS = "e.visibility-event-elapsed-real-time"; + /** + * @since 4.8.0 + */ + public static final String EXTRA_TARGET_URI_FOR_REPORT_DEVICE_STATES = "e.target-uri-for-report-device-states"; + + /** + * @since 4.8.0 + */ + public static final String ATTRIBUTE_KEY_BUILD_ENVIRONMENT = "ak.build-environment"; + + /** + * @since 4.8.0 + */ + public static final String ATTRIBUTE_KEY_RUNTIME_EXTRAS = "ak.runtime-extras"; + + /** + * @since 4.8.0 + */ + public static final String ATTRIBUTE_KEY_SDK_DEVICE_STATES = "ak.sdk-device-states"; + + /** + * @since 4.8.0 + */ + public static final String ATTRIBUTE_KEY_EVENT_AT = "ak.event-at"; + interface VisibilityType { int BACKGROUND = 0; int FOREGROUND = 1; diff --git a/sdk/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java b/sdk/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java new file mode 100644 index 0000000..b75071b --- /dev/null +++ b/sdk/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java @@ -0,0 +1,65 @@ +package com.deploygate.sdk; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * This test class will make sure all *public* interfaces are defined as expected + */ +@RunWith(AndroidJUnit4.class) +public class CustomAttributesInterfaceTest { + + @NonNull + CustomAttributes attributes; + + @Before + public void setUp() { + attributes = new CustomAttributes(); + } + + @Test + public void putString() { + Truth.assertThat(attributes.putString("key", "value")).isInstanceOf(Boolean.class); + } + + @Test + public void putInt() { + Truth.assertThat(attributes.putInt("key", 1)).isInstanceOf(Boolean.class); + } + + @Test + public void putLong() { + Truth.assertThat(attributes.putLong("key", 1L)).isInstanceOf(Boolean.class); + } + + @Test + public void putFloat() { + Truth.assertThat(attributes.putFloat("key", 1.0f)).isInstanceOf(Boolean.class); + } + + @Test + public void putDouble() { + Truth.assertThat(attributes.putDouble("key", 1.0)).isInstanceOf(Boolean.class); + } + + @Test + public void putBoolean() { + Truth.assertThat(attributes.putBoolean("key", true)).isInstanceOf(Boolean.class); + } + + @Test + public void remove() { + attributes.remove("key"); + } + + @Test + public void removeAll() { + attributes.removeAll(); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/com/deploygate/sdk/CustomAttributesTest.java b/sdk/src/test/java/com/deploygate/sdk/CustomAttributesTest.java new file mode 100644 index 0000000..9ac62eb --- /dev/null +++ b/sdk/src/test/java/com/deploygate/sdk/CustomAttributesTest.java @@ -0,0 +1,203 @@ +package com.deploygate.sdk; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@RunWith(AndroidJUnit4.class) +public class CustomAttributesTest { + + @NonNull + CustomAttributes attributes; + + @Before + public void setUp() { + attributes = new CustomAttributes(); + } + + @Test + public void put__accept_when_valid_key() { + Truth.assertThat(attributes.putString("valid", "valid_value1")).isTrue(); + Truth.assertThat(attributes.putString("valid_underscore", "valid_value2")).isTrue(); + Truth.assertThat(attributes.putString("valid_1_number", "valid_value3")).isTrue(); + Truth.assertThat(attributes.putString("min", "valid_value4")).isTrue(); + Truth.assertThat(attributes.putString("valid_key_with_length_under_32", "valid_value5")).isTrue(); + + Truth.assertThat(attributes.putString("ng", "invalid_value1")).isFalse(); + Truth.assertThat(attributes.putString("true", "invalid_value2")).isFalse(); + Truth.assertThat(attributes.putString("false", "invalid_value3")).isFalse(); + Truth.assertThat(attributes.putString("null", "invalid_value4")).isFalse(); + Truth.assertThat(attributes.putString("invalid-hyphen", "invalid_value5")).isFalse(); + Truth.assertThat(attributes.putString("invalid#sharp", "invalid_value6")).isFalse(); + Truth.assertThat(attributes.putString("invalid$dollar", "invalid_value7")).isFalse(); + Truth.assertThat(attributes.putString("invalid.dot", "invalid_value8")).isFalse(); + Truth.assertThat(attributes.putString("invalid!bang", "invalid_value9")).isFalse(); + Truth.assertThat(attributes.putString("invalid*glob", "invalid_value10")).isFalse(); + Truth.assertThat(attributes.putString("invalidUpperCase", "invalid_value11")).isFalse(); + Truth.assertThat(attributes.putString("12345", "invalid_value12")).isFalse(); + Truth.assertThat(attributes.putString("1_invalid_begin_number", "invalid_value13")).isFalse(); + Truth.assertThat(attributes.putString("invalid_key_with_length_over_32_characters", "invalid_value14")).isFalse(); + } + + @Test + public void put__accept_when_valid_value() { + Truth.assertThat(attributes.putString("valid_string", "value")).isTrue(); + Truth.assertThat(attributes.putInt("valid_int", 1)).isTrue(); + Truth.assertThat(attributes.putLong("valid_long", 1L)).isTrue(); + Truth.assertThat(attributes.putFloat("valid_float", 1.1f)).isTrue(); + Truth.assertThat(attributes.putDouble("valid_double", 1.1)).isTrue(); + Truth.assertThat(attributes.putBoolean("valid_boolean", true)).isTrue(); + + String tooLongString = "this is too long string value. we cannot accept value if size over 64."; + Truth.assertThat(attributes.putString("invalid_too_long_string", "this is too long string value. we cannot accept value if size over 64.")).isFalse(); + } + + @Test + public void size() { + Truth.assertThat(attributes.size()).isEqualTo(0); + + attributes.putString("key1", "value1"); + Truth.assertThat(attributes.size()).isEqualTo(1); + + attributes.putString("key1", "overwrite1"); + Truth.assertThat(attributes.size()).isEqualTo(1); + + attributes.putString("key2", "value2"); + attributes.putString("key3", "value3"); + attributes.putString("key4", "value4"); + attributes.putString("key5", "value5"); + attributes.putString("key6", "value6"); + attributes.putString("key7", "value7"); + attributes.putString("key8", "value8"); + Truth.assertThat(attributes.size()).isEqualTo(8); + + attributes.putString("key9", "value9"); + Truth.assertThat(attributes.size()).isEqualTo(8); + + attributes.remove("key1"); + Truth.assertThat(attributes.size()).isEqualTo(7); + + attributes.removeAll(); + Truth.assertThat(attributes.size()).isEqualTo(0); + } + + @Test + public void toJSONString() { + Truth.assertThat(attributes.getJSONString()).isEqualTo("{}"); + + attributes.putString("string", "value"); + attributes.putInt("int", Integer.MAX_VALUE); + attributes.putLong("long", Long.MAX_VALUE); + attributes.putFloat("float", Float.MAX_VALUE); + attributes.putDouble("double", Double.MAX_VALUE); + attributes.putBoolean("boolean", true); + + String expectedJSON = "{" + + "\"string\":\"value\"," + + "\"int\":2147483647," + + "\"long\":9223372036854775807," + + "\"float\":3.4028235E38," + + "\"double\":1.7976931348623157E308," + + "\"boolean\":true" + + "}"; + Truth.assertThat(attributes.getJSONString()).isEqualTo(expectedJSON); + + attributes.removeAll(); + attributes.putString("string2", "value2"); + attributes.putInt("int2", Integer.MIN_VALUE); + attributes.putLong("long2", Long.MIN_VALUE); + attributes.putFloat("float2", Float.MIN_VALUE); + attributes.putDouble("double2", Double.MIN_VALUE); + attributes.putBoolean("boolean2", false); + + String expectedJSON2 = "{" + + "\"string2\":\"value2\"," + + "\"int2\":-2147483648," + + "\"long2\":-9223372036854775808," + + "\"float2\":1.4E-45," + + "\"double2\":4.9E-324," + + "\"boolean2\":false" + + "}"; + Truth.assertThat(attributes.getJSONString()).isEqualTo(expectedJSON2); + } + + @Test + public void not_exceed_max_size() { + Truth.assertThat(attributes.putString("key1", "value1")).isTrue(); + Truth.assertThat(attributes.putString("key2", "value2")).isTrue(); + Truth.assertThat(attributes.putString("key3", "value3")).isTrue(); + Truth.assertThat(attributes.putString("key4", "value4")).isTrue(); + Truth.assertThat(attributes.putString("key5", "value5")).isTrue(); + Truth.assertThat(attributes.putString("key6", "value6")).isTrue(); + Truth.assertThat(attributes.putString("key7", "value7")).isTrue(); + Truth.assertThat(attributes.putString("key8", "value8")).isTrue(); + + // allow to overwrite + Truth.assertThat(attributes.putString("key1", "overwrite1_1")).isTrue(); + // not allow to put value with new key because of max size + Truth.assertThat(attributes.putString("key9", "value9")).isFalse(); + + attributes.remove("key8"); + + // allow to put value with new key after remove exists key + Truth.assertThat(attributes.putString("key9", "value9")).isTrue(); + // allow to overwrite + Truth.assertThat(attributes.putString("key1", "overwrite1_2")).isTrue(); + // not allow to put value with new key because of max size + Truth.assertThat(attributes.putString("key10", "value10")).isFalse(); + + attributes.removeAll(); + + // allow to put value less than max size + Truth.assertThat(attributes.putString("key1", "another_value1")).isTrue(); + Truth.assertThat(attributes.putString("key2", "another_value2")).isTrue(); + Truth.assertThat(attributes.putString("key3", "another_value3")).isTrue(); + Truth.assertThat(attributes.putString("key4", "another_value4")).isTrue(); + Truth.assertThat(attributes.putString("key5", "another_value5")).isTrue(); + Truth.assertThat(attributes.putString("key6", "another_value6")).isTrue(); + Truth.assertThat(attributes.putString("key7", "another_value7")).isTrue(); + Truth.assertThat(attributes.putString("key8", "another_value8")).isTrue(); + Truth.assertThat(attributes.putString("key9", "another_value9")).isFalse(); + } + + @Test() + public void not_exceed_max_size_multi_thread() { + // prepare attributes with max size + for (int i = 0; i < 8; i++) { + attributes.putString("key" + i, "value" + i); + } + + // try to put value with multi thread + ExecutorService executors = Executors.newCachedThreadPool(); + for (int i = 0; i < 100; i++) { + final int index = i; + executors.submit(new Runnable() { + @Override + public void run() { + attributes.putString("key" + index, "value" + index); + } + }); + } + + Truth.assertThat(attributes.size()).isEqualTo(8); + String expectedJSON = "{" + + "\"key0\":\"value0\"," + + "\"key1\":\"value1\"," + + "\"key2\":\"value2\"," + + "\"key3\":\"value3\"," + + "\"key4\":\"value4\"," + + "\"key5\":\"value5\"," + + "\"key6\":\"value6\"," + + "\"key7\":\"value7\"" + + "}"; + Truth.assertThat(attributes.getJSONString()).isEqualTo(expectedJSON); + } +} diff --git a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java index 90622bc..62684ee 100644 --- a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java @@ -364,4 +364,14 @@ public void composeComment__String() { public void getDistributionUserName() { Truth.assertThat(DeployGate.getDistributionUserName()).isNull(); } + + @Test + public void getBuildEnvironment() { + Truth.assertThat(DeployGate.getBuildEnvironment()).isNotNull(); + } + + @Test + public void getRuntimeExtra() { + Truth.assertThat(DeployGate.getRuntimeExtra()).isNotNull(); + } } diff --git a/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java b/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java index a4f2999..5793449 100644 --- a/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java @@ -36,7 +36,7 @@ public void default_properties() { Truth.assertThat(app.canUseLogcat).isTrue(); Truth.assertThat(app.packageName).isEqualTo("com.deploygate.sdk.test"); Truth.assertThat(app.sdkVersion).isEqualTo(4); - Truth.assertThat(app.sdkArtifactVersion).isEqualTo("4.7.1"); + Truth.assertThat(app.sdkArtifactVersion).isEqualTo("4.8.0"); Truth.assertThat(app.activeFeatureFlags).isEqualTo(FULL_BIT); Truth.assertThat(app.canUseDeviceCapture()).isTrue(); } diff --git a/sdkMock/src/main/java/com/deploygate/sdk/CustomAttributes.java b/sdkMock/src/main/java/com/deploygate/sdk/CustomAttributes.java new file mode 100644 index 0000000..cdfc4f2 --- /dev/null +++ b/sdkMock/src/main/java/com/deploygate/sdk/CustomAttributes.java @@ -0,0 +1,37 @@ +package com.deploygate.sdk; + +public final class CustomAttributes { + + CustomAttributes() { + } + + public boolean putString(String key, String value) { + return false; + } + + public boolean putInt(String key, int value) { + return false; + } + + public boolean putLong(String key, long value) { + return false; + } + + public boolean putFloat(String key, float value) { + return false; + } + + public boolean putDouble(String key, double value) { + return false; + } + + public boolean putBoolean(String key, boolean value) { + return false; + } + + public void remove(String key) { + } + + public void removeAll() { + } +} diff --git a/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java b/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java index 22e096f..5d0b765 100644 --- a/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java +++ b/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java @@ -5,6 +5,8 @@ public class DeployGate { + private static final CustomAttributes sAttributes = new CustomAttributes(); + static void clear() { } @@ -179,4 +181,12 @@ public static void composeComment(String defaultComment) { public static String getDistributionUserName() { return null; } + + public static CustomAttributes getBuildEnvironment() { + return sAttributes; + } + + public static CustomAttributes getRuntimeExtra() { + return sAttributes; + } } diff --git a/sdkMock/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java b/sdkMock/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java new file mode 120000 index 0000000..cc4e30b --- /dev/null +++ b/sdkMock/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java @@ -0,0 +1 @@ +../../../../../../../sdk/src/test/java/com/deploygate/sdk/CustomAttributesInterfaceTest.java \ No newline at end of file