diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2d02ad3..ab3d7ab 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -36,7 +36,7 @@ jobs:
steps:
- checkout
- restore_gradle_cache
- - run: ./gradlew sdk:build sdkMock:build --continue
+ - run: ./gradlew sdk:build sdkMock:build -x sdk:test -x sdkMock:test --continue
- save_gradle_cache
test:
executor: android
diff --git a/.gitignore b/.gitignore
index c61d2d0..12bf2a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@ build/
#idea
*.iml
.idea/
+
+.DS_Store
\ No newline at end of file
diff --git a/sample/proguard-project.txt b/sample/proguard-project.txt
index 8bb8ea0..3b27d29 100644
--- a/sample/proguard-project.txt
+++ b/sample/proguard-project.txt
@@ -19,3 +19,8 @@
# public *;
#}
-keep class * extends android.os.IInterface
+
+# to check the R8 results except obfuscation
+-keepnames class com.deploygate.sdk.** {
+ *;
+}
\ No newline at end of file
diff --git a/sdk/build.gradle b/sdk/build.gradle
index 6493ebc..c81259c 100644
--- a/sdk/build.gradle
+++ b/sdk/build.gradle
@@ -1,6 +1,14 @@
apply from: rootProject.file('sdk.build.gradle')
-android.defaultConfig.consumerProguardFiles 'deploygate-sdk-proguard-rules.txt'
+android {
+ defaultConfig {
+ consumerProguardFiles 'deploygate-sdk-proguard-rules.txt'
+ }
+}
+
+dependencies {
+ testImplementation 'org.mockito:mockito-core:4.4.0'
+}
ext {
displayName = "DeployGate SDK"
diff --git a/sdk/deploygate-sdk-proguard-rules.txt b/sdk/deploygate-sdk-proguard-rules.txt
index 564865b..7512270 100644
--- a/sdk/deploygate-sdk-proguard-rules.txt
+++ b/sdk/deploygate-sdk-proguard-rules.txt
@@ -1 +1,10 @@
-keep class * extends android.os.IInterface
+
+# To allow removing our logger
+-assumenosideeffects class com.deploygate.sdk.internal.Logger {
+ public static void v(...);
+ public static void i(...);
+ public static void w(...);
+ public static void d(...);
+ public static void e(...);
+}
diff --git a/sdk/src/debug/java/com/deploygate/sdk/internal/Config.java b/sdk/src/debug/java/com/deploygate/sdk/internal/Config.java
new file mode 100644
index 0000000..bf9fb0f
--- /dev/null
+++ b/sdk/src/debug/java/com/deploygate/sdk/internal/Config.java
@@ -0,0 +1,5 @@
+package com.deploygate.sdk.internal;
+
+public class Config {
+ public static final boolean DEBUG = true;
+}
diff --git a/sdk/src/main/java/com/deploygate/sdk/CustomLog.java b/sdk/src/main/java/com/deploygate/sdk/CustomLog.java
new file mode 100644
index 0000000..d8313ad
--- /dev/null
+++ b/sdk/src/main/java/com/deploygate/sdk/CustomLog.java
@@ -0,0 +1,37 @@
+package com.deploygate.sdk;
+
+import android.os.Bundle;
+
+import com.deploygate.service.DeployGateEvent;
+
+class CustomLog {
+ public final String type;
+ public final String body;
+ private int retryCount;
+
+ CustomLog(
+ String type,
+ String body
+ ) {
+ this.type = type;
+ this.body = body;
+ this.retryCount = 0;
+ }
+
+ /**
+ * @return the number of current attempts
+ */
+ int getAndIncrementRetryCount() {
+ return retryCount++;
+ }
+
+ /**
+ * @return a bundle to send to the client service
+ */
+ Bundle toExtras() {
+ Bundle extras = new Bundle();
+ extras.putSerializable(DeployGateEvent.EXTRA_LOG, body);
+ extras.putSerializable(DeployGateEvent.EXTRA_LOG_TYPE, type);
+ return extras;
+ }
+}
diff --git a/sdk/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java b/sdk/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java
new file mode 100644
index 0000000..0b2fd69
--- /dev/null
+++ b/sdk/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java
@@ -0,0 +1,91 @@
+package com.deploygate.sdk;
+
+import com.deploygate.sdk.internal.Logger;
+
+public class CustomLogConfiguration {
+ public enum Backpressure {
+ /**
+ * SDK rejects new logs if buffer size is exceeded
+ */
+ PRESERVE_BUFFER,
+
+ /**
+ * SDK drops logs from the oldest if buffer size is exceeded
+ */
+ DROP_BUFFER_BY_OLDEST
+ }
+
+ /**
+ * the log buffer is required until DeployGate client app receives BOOT_COMPLETED broadcast.
+ *
+ * This is an experimental value.
+ *
+ * - 10 seconds until boot-completed
+ * - 10 logs per 1 seconds
+ * - plus some buffer
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 150;
+ private static final int MAX_BUFFER_SIZE = DEFAULT_BUFFER_SIZE;
+
+ public final Backpressure backpressure;
+ public final int bufferSize;
+
+ /**
+ * Do not bypass {@link Builder} to instantiate this class.
+ *
+ * @see Builder
+ */
+ private CustomLogConfiguration(
+ Backpressure backpressure,
+ int bufferSize
+ ) {
+ this.backpressure = backpressure;
+ this.bufferSize = bufferSize;
+ }
+
+ public static class Builder {
+ private Backpressure backpressure = Backpressure.DROP_BUFFER_BY_OLDEST;
+ private int bufferSize = DEFAULT_BUFFER_SIZE;
+
+ /**
+ * @param backpressure
+ * the strategy of the backpressure in the log buffer
+ *
+ * @return self
+ *
+ * @see Backpressure
+ */
+ public Builder setBackpressure(Backpressure backpressure) {
+ if (backpressure == null) {
+ throw new IllegalArgumentException("backpressure must be non-null");
+ }
+
+ this.backpressure = backpressure;
+ return this;
+ }
+
+ /**
+ * @param bufferSize
+ * the max size of the log buffer
+ *
+ * @return self
+ */
+ public Builder setBufferSize(int bufferSize) {
+ if (bufferSize <= 0) {
+ throw new IllegalArgumentException("buffer size must be greater than 0");
+ }
+
+ if (bufferSize > MAX_BUFFER_SIZE) {
+ Logger.w("buffer size is exceeded %d so it's rounded to %d", bufferSize, MAX_BUFFER_SIZE);
+ bufferSize = MAX_BUFFER_SIZE;
+ }
+
+ this.bufferSize = bufferSize;
+ return this;
+ }
+
+ public CustomLogConfiguration build() {
+ return new CustomLogConfiguration(backpressure, bufferSize);
+ }
+ }
+}
diff --git a/sdk/src/main/java/com/deploygate/sdk/CustomLogInstructionSerializer.java b/sdk/src/main/java/com/deploygate/sdk/CustomLogInstructionSerializer.java
new file mode 100644
index 0000000..76455dd
--- /dev/null
+++ b/sdk/src/main/java/com/deploygate/sdk/CustomLogInstructionSerializer.java
@@ -0,0 +1,351 @@
+package com.deploygate.sdk;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import com.deploygate.sdk.internal.Logger;
+import com.deploygate.service.DeployGateEvent;
+import com.deploygate.service.IDeployGateSdkService;
+
+import java.util.LinkedList;
+
+/**
+ * This class serialize the instructions for custom logs and process them in another thread in order of enqueued.
+ * The internal handler class creates a buffer pool and guarantee the order.
+ *
+ *
+ * - Enqueue a new log to the buffer pool
+ * - Send logs in the order of FIFO
+ *
+ * The cost of polling the log buffer pool may be high, so SDK starts sending logs only while a service is active.
+ */
+class CustomLogInstructionSerializer {
+ static final int MAX_RETRY_COUNT = 2;
+ static final int SEND_LOG_RESULT_SUCCESS = 0;
+ static final int SEND_LOG_RESULT_FAILURE_RETRIABLE = -1;
+ static final int SEND_LOG_RESULT_FAILURE_RETRY_EXCEEDED = -2;
+
+ private final String packageName;
+ private final CustomLogConfiguration configuration;
+
+ @SuppressWarnings("FieldCanBeLocal")
+ private final HandlerThread thread;
+ private CustomLogHandler handler;
+ private boolean isDisabled;
+
+ private volatile IDeployGateSdkService service;
+
+ CustomLogInstructionSerializer(
+ String packageName,
+ CustomLogConfiguration configuration
+ ) {
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("packageName must be present");
+ }
+
+ if (configuration == null) {
+ throw new IllegalArgumentException("configuration must not be null");
+ }
+
+ this.packageName = packageName;
+ this.configuration = configuration;
+ this.isDisabled = false;
+
+ this.thread = new HandlerThread("deploygate-sdk-custom-log");
+ this.thread.start();
+ }
+
+ /**
+ * Bind a service and trigger several instructions immediately.
+ *
+ * @param service
+ * the latest service connection
+ */
+ public final synchronized void connect(IDeployGateSdkService service) {
+ if (service == null) {
+ throw new IllegalArgumentException("service must not be null");
+ }
+
+ ensureHandlerInitialized();
+
+ handler.cancelPendingSendLogsInstruction();
+ this.service = service;
+ handler.enqueueSendLogsInstruction();
+ }
+
+ /**
+ * Release a service connection and cancel all pending instructions.
+ */
+ public final void disconnect() {
+ ensureHandlerInitialized();
+
+ handler.cancelPendingSendLogsInstruction();
+ this.service = null;
+ }
+
+ /**
+ * Request sending custom logs to DeployGate client service. All requests will be scheduled to an exclusive thread.
+ *
+ * @param type
+ * custom log type
+ * @param body
+ * custom log body
+ */
+ public final synchronized void requestSendingLog(
+ String type,
+ String body
+ ) {
+ if (isDisabled) {
+ return;
+ }
+
+ ensureHandlerInitialized();
+
+ CustomLog log = new CustomLog(type, body);
+ handler.enqueueAddNewLogInstruction(log);
+ }
+
+ /**
+ * Disable accepting instructions. This does not mean interrupting and/or terminating the exclusive thread.
+ *
+ * @param isDisabled
+ * specify true if wanna disable this serializer, otherwise false.
+ */
+ public final void setDisabled(boolean isDisabled) {
+ this.isDisabled = isDisabled;
+
+ if (isDisabled) {
+ Logger.d("Disabled custom log instruction serializer");
+ } else {
+ Logger.d("Enabled custom log instruction serializer");
+ }
+ }
+
+ /**
+ * Check if this serializer a service connection.
+ *
+ * The connection may return {@link android.os.DeadObjectException} even if this returns true;
+ *
+ * @return true if a service connection is assigned, otherwise false.
+ */
+ public final boolean hasServiceConnection() {
+ return service != null;
+ }
+
+ /**
+ * @return
+ *
+ * @hide Only for testing.
+ */
+ Looper getLooper() {
+ return handler.getLooper();
+ }
+
+ boolean hasHandlerInitialized() {
+ return handler != null;
+ }
+
+ int getPendingCount() {
+ if (handler == null) {
+ return 0;
+ }
+
+ return handler.customLogs.size();
+ }
+
+ /**
+ * Send a single log to the receiver.
+ *
+ * SDK can't send logs in bulk to avoid TransactionTooLargeException
+ *
+ * Visible only for testing
+ *
+ * @param log
+ * a custom log to send
+ *
+ * @return true if this could send the custom log, otherwise false.
+ */
+ int sendLog(CustomLog log) {
+ IDeployGateSdkService service = this.service;
+
+ if (service == null) {
+ // Don't increment retry count
+ return SEND_LOG_RESULT_FAILURE_RETRIABLE;
+ }
+
+ try {
+ service.sendEvent(packageName, DeployGateEvent.ACTION_SEND_CUSTOM_LOG, log.toExtras());
+ return SEND_LOG_RESULT_SUCCESS;
+ } catch (RemoteException e) {
+ int currentAttempts = log.getAndIncrementRetryCount();
+
+ if (currentAttempts >= MAX_RETRY_COUNT) {
+ Logger.e("failed to send custom log and exceeded the max retry count: %s", e.getMessage());
+ return SEND_LOG_RESULT_FAILURE_RETRY_EXCEEDED;
+ } else {
+ Logger.w("failed to send custom log %d times: %s", currentAttempts + 1, e.getMessage());
+ }
+
+ return SEND_LOG_RESULT_FAILURE_RETRIABLE;
+ }
+ }
+
+ private void ensureHandlerInitialized() {
+ if (handler != null) {
+ return;
+ }
+
+ synchronized (configuration) {
+ if (handler != null) {
+ return;
+ }
+
+ handler = new CustomLogHandler(thread.getLooper(), this, configuration.backpressure, configuration.bufferSize);
+ }
+ }
+
+ /**
+ * This handler behaves as a ordered-buffer of instructions.
+ *
+ * The instruction of adding a new log and sending buffered logs are synchronized.
+ */
+ private static class CustomLogHandler extends Handler {
+ private static final int WHAT_SEND_LOGS = 0x30;
+ private static final int WHAT_ADD_NEW_LOG = 0x100;
+
+ private final CustomLogInstructionSerializer serializer;
+ private final CustomLogConfiguration.Backpressure backpressure;
+ private final int bufferSize;
+ private final LinkedList customLogs;
+
+ /**
+ * @param looper
+ * Do not use Main Looper to avoid wasting the main thread resource.
+ * @param serializer
+ * an instance to send logs
+ * @param backpressure
+ * the backpressure strategy of the log buffer, not of instructions.
+ * @param bufferSize
+ * the max size of the log buffer, not of instructions.
+ */
+ CustomLogHandler(
+ Looper looper,
+ CustomLogInstructionSerializer serializer,
+ CustomLogConfiguration.Backpressure backpressure,
+ int bufferSize
+ ) {
+ super(looper);
+ this.serializer = serializer;
+ this.backpressure = backpressure;
+ this.bufferSize = bufferSize;
+ this.customLogs = new LinkedList<>();
+ }
+
+ /**
+ * Cancel the send-logs instruction in the handler message queue.
+ * This doesn't interrupt the thread and stop the sending-logs instruction that is running at the time.
+ */
+ void cancelPendingSendLogsInstruction() {
+ removeMessages(WHAT_SEND_LOGS);
+ }
+
+ /**
+ * Enqueue the send-logs instruction in the handler message queue unless enqueued.
+ */
+ void enqueueSendLogsInstruction() {
+ if (hasMessages(WHAT_SEND_LOGS)) {
+ return;
+ }
+
+ sendEmptyMessage(WHAT_SEND_LOGS);
+ }
+
+ /**
+ * Enqueue new add-new-log instruction to the handler message queue.
+ */
+ void enqueueAddNewLogInstruction(CustomLog log) {
+ Message msg = obtainMessage(WHAT_ADD_NEW_LOG, log);
+ sendMessage(msg);
+ }
+
+ /**
+ * Append the new log to the log buffer if the backpressure is not dropping LATEST.
+ *
+ * @param log
+ * a new log
+ */
+ void addLogToLast(CustomLog log) {
+ boolean dropFirst = backpressure == CustomLogConfiguration.Backpressure.DROP_BUFFER_BY_OLDEST;
+ int droppedCount = 0;
+
+ while (customLogs.size() >= bufferSize) {
+ if (dropFirst) {
+ customLogs.poll();
+ droppedCount++;
+ } else {
+ Logger.d("the log buffer is already full and reject the new log.");
+ return;
+ }
+ }
+
+ Logger.d("filtered out %d overflowed logs from the oldest.", droppedCount);
+
+ customLogs.addLast(log);
+
+ if (serializer.hasServiceConnection()) {
+ sendAllInBuffer();
+ }
+ }
+
+ /**
+ * send all logs from the oldest in the log buffers
+ * If sending a log failed, it will be put into the head of the log buffer. And then, this schedules the sending-logs instruction with some delay.
+ */
+ void sendAllInBuffer() {
+ boolean retry = false;
+
+ while (!customLogs.isEmpty()) {
+ CustomLog log = customLogs.poll();
+
+ if (serializer.sendLog(log) == SEND_LOG_RESULT_FAILURE_RETRIABLE) {
+ // Don't lost the failed log
+ customLogs.addFirst(log);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ try {
+ removeMessages(WHAT_SEND_LOGS);
+ Message msg = obtainMessage(WHAT_SEND_LOGS);
+ // Put the retry message at front of the queue because delay or enqueuing a message may cause unexpected overflow of the buffer.
+ sendMessageAtFrontOfQueue(msg);
+ Thread.sleep(600); // experimental valuea
+ } catch (InterruptedException ignore) {
+ }
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case WHAT_SEND_LOGS: {
+ sendAllInBuffer();
+
+ break;
+ }
+ case WHAT_ADD_NEW_LOG: {
+ CustomLog log = (CustomLog) msg.obj;
+ addLogToLast(log);
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java
index b333aba..7888258 100644
--- a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java
+++ b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java
@@ -48,7 +48,6 @@
* @author tnj
*/
public class DeployGate {
-
private static final String TAG = "DeployGate";
private static final int SDK_VERSION = 4;
@@ -70,6 +69,7 @@ public class DeployGate {
private final Context mApplicationContext;
private final Handler mHandler;
+ private final CustomLogInstructionSerializer mCustomLogInstructionSerializer;
private final HashSet mCallbacks;
private final String mExpectedAuthor;
private String mAuthor;
@@ -116,8 +116,6 @@ public void onEvent(
}
}
- ;
-
private void onInitialized(
final boolean isManaged,
final boolean isAuthorized,
@@ -226,10 +224,12 @@ public void run() {
private DeployGate(
Context applicationContext,
String author,
- DeployGateCallback callback
+ DeployGateCallback callback,
+ CustomLogConfiguration customLogConfiguration
) {
- mHandler = new Handler();
mApplicationContext = applicationContext;
+ mHandler = new Handler();
+ mCustomLogInstructionSerializer = new CustomLogInstructionSerializer(mApplicationContext.getPackageName(), customLogConfiguration);
mCallbacks = new HashSet();
mExpectedAuthor = author;
@@ -246,10 +246,12 @@ private DeployGate(
private boolean initService(boolean isBoot) {
if (isDeployGateAvailable()) {
Log.v(TAG, "DeployGate installation detected. Initializing.");
+ mCustomLogInstructionSerializer.setDisabled(false);
bindToService(isBoot);
return true;
} else {
Log.v(TAG, "DeployGate is not available on this device.");
+ mCustomLogInstructionSerializer.setDisabled(true);
mInitializedLatch.countDown();
mIsDeployGateAvailable = false;
callbackDeployGateUnavailable();
@@ -305,6 +307,7 @@ public void onServiceConnected(
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "DeployGate service disconneced");
mRemoteService = null;
+ mCustomLogInstructionSerializer.disconnect();
}
}, Context.BIND_AUTO_CREATE);
}
@@ -317,6 +320,7 @@ private void requestServiceInit(final boolean isBoot) {
args.putInt(DeployGateEvent.EXTRA_SDK_VERSION, SDK_VERSION);
try {
mRemoteService.init(mRemoteCallback, mApplicationContext.getPackageName(), args);
+ mCustomLogInstructionSerializer.connect(mRemoteService);
} catch (RemoteException e) {
Log.w(TAG, "DeployGate service failed to be initialized.");
}
@@ -566,6 +570,35 @@ public static void install(
String author,
DeployGateCallback callback,
boolean forceApplyOnReleaseBuild
+ ) {
+ install(app, author, callback, forceApplyOnReleaseBuild, new CustomLogConfiguration.Builder().build());
+ }
+
+ /**
+ * Install DeployGate on your application instance and register a callback
+ * listener. Call this method inside of your {@link Application#onCreate()}
+ * once.
+ *
+ * @param app
+ * Application instance, typically just pass this.
+ * @param author
+ * author username of this app. Can be null.
+ * @param callback
+ * Callback interface to listen events. Can be null.
+ * @param forceApplyOnReleaseBuild
+ * if you want to keep DeployGate alive on
+ * the release build, set this true.
+ * @param customLogConfiguration
+ * set a configuration for custom logging
+ *
+ * @since r4.4
+ */
+ public static void install(
+ Application app,
+ String author,
+ DeployGateCallback callback,
+ boolean forceApplyOnReleaseBuild,
+ CustomLogConfiguration customLogConfiguration
) {
if (sInstance != null) {
Log.w(TAG, "DeployGate.install was already called. Ignoring.");
@@ -577,7 +610,7 @@ public static void install(
}
Thread.setDefaultUncaughtExceptionHandler(new DeployGateUncaughtExceptionHandler(Thread.getDefaultUncaughtExceptionHandler()));
- sInstance = new DeployGate(app.getApplicationContext(), author, callback);
+ sInstance = new DeployGate(app.getApplicationContext(), author, callback, customLogConfiguration);
}
/**
@@ -1065,15 +1098,7 @@ void sendLog(
String type,
String body
) {
- Bundle extras = new Bundle();
- extras.putSerializable(DeployGateEvent.EXTRA_LOG, body);
- extras.putSerializable(DeployGateEvent.EXTRA_LOG_TYPE, type);
-
- try {
- mRemoteService.sendEvent(mApplicationContext.getPackageName(), DeployGateEvent.ACTION_SEND_CUSTOM_LOG, extras);
- } catch (RemoteException e) {
- Log.w(TAG, "failed to send custom log: " + e.getMessage());
- }
+ mCustomLogInstructionSerializer.requestSendingLog(type, body);
}
/**
diff --git a/sdk/src/main/java/com/deploygate/sdk/internal/Logger.java b/sdk/src/main/java/com/deploygate/sdk/internal/Logger.java
new file mode 100644
index 0000000..64ed6b0
--- /dev/null
+++ b/sdk/src/main/java/com/deploygate/sdk/internal/Logger.java
@@ -0,0 +1,53 @@
+package com.deploygate.sdk.internal;
+
+import android.util.Log;
+
+import java.util.Locale;
+
+public class Logger {
+ private static final String TAG = "DeployGateSDK";
+
+ public static void d(
+ String format,
+ Object... args
+ ) {
+ Log.d(TAG, String.format(Locale.US, format, args));
+ }
+
+ public static void i(
+ String format,
+ Object... args
+ ) {
+ Log.i(TAG, String.format(Locale.US, format, args));
+ }
+
+ public static void w(
+ String format,
+ Object... args
+ ) {
+ Log.w(TAG, String.format(Locale.US, format, args));
+ }
+
+ public static void w(
+ Throwable th,
+ String format,
+ Object... args
+ ) {
+ Log.w(TAG, String.format(Locale.US, format, args), th);
+ }
+
+ public static void e(
+ String format,
+ Object... args
+ ) {
+ Log.e(TAG, String.format(Locale.US, format, args));
+ }
+
+ public static void e(
+ Throwable th,
+ String format,
+ Object... args
+ ) {
+ Log.e(TAG, String.format(Locale.US, format, args), th);
+ }
+}
diff --git a/sdk/src/release/java/com/deploygate/sdk/internal/Config.java b/sdk/src/release/java/com/deploygate/sdk/internal/Config.java
new file mode 100644
index 0000000..6b1aeef
--- /dev/null
+++ b/sdk/src/release/java/com/deploygate/sdk/internal/Config.java
@@ -0,0 +1,5 @@
+package com.deploygate.sdk.internal;
+
+public class Config {
+ public static final boolean DEBUG = false;
+}
diff --git a/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java b/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java
new file mode 100644
index 0000000..67d8571
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java
@@ -0,0 +1,249 @@
+package com.deploygate.sdk;
+
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.deploygate.sdk.truth.BundleSubject;
+import com.deploygate.service.DeployGateEvent;
+import com.deploygate.service.FakeDeployGateClientService;
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.verification.VerificationMode;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.LooperMode;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static com.deploygate.sdk.mockito.BundleMatcher.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
+
+@RunWith(AndroidJUnit4.class)
+@LooperMode(PAUSED)
+public class CustomLogInstructionSerializerTest {
+
+ private static final String PACKAGE_NAME = "com.deploygate.sample";
+
+ private FakeDeployGateClientService service;
+
+ @Before
+ public void before() {
+ service = Mockito.spy(new FakeDeployGateClientService(PACKAGE_NAME));
+ }
+
+ @Test(timeout = 3000L)
+ public void check_buffer_size_works_with_drop_by_oldest() throws RemoteException, InterruptedException {
+ final int bufferSize = 5;
+
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().setBufferSize(bufferSize).setBackpressure(CustomLogConfiguration.Backpressure.DROP_BUFFER_BY_OLDEST).build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ for (int i = 0; i < 10; i++) {
+ customLogInstructionSerializer.requestSendingLog("type", String.valueOf(i));
+ }
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ customLogInstructionSerializer.connect(service);
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ for (int i = 0; i < bufferSize; i++) {
+ CustomLog log = new CustomLog("type", String.valueOf(i));
+ Mockito.verify(service, never()).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(log.toExtras()));
+ }
+
+ VerificationMode once = Mockito.times(1);
+ InOrder inOrder = Mockito.inOrder(service);
+
+ for (int i = bufferSize; i < 10; i++) {
+ CustomLog log = new CustomLog("type", String.valueOf(i));
+ inOrder.verify(service, once).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(log.toExtras()));
+ }
+ }
+
+ @Test(timeout = 3000L)
+ public void check_buffer_size_works_with_preserve_buffer() throws RemoteException {
+ final int bufferSize = 5;
+
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().setBufferSize(bufferSize).setBackpressure(CustomLogConfiguration.Backpressure.PRESERVE_BUFFER).build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ for (int i = 0; i < 10; i++) {
+ customLogInstructionSerializer.requestSendingLog("type", String.valueOf(i));
+ }
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ customLogInstructionSerializer.connect(service);
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ VerificationMode once = Mockito.times(1);
+ InOrder inOrder = Mockito.inOrder(service);
+
+ for (int i = 0; i < bufferSize; i++) {
+ CustomLog log = new CustomLog("type", String.valueOf(i));
+ inOrder.verify(service, once).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(log.toExtras()));
+ }
+
+ for (int i = bufferSize; i < 10; i++) {
+ CustomLog log = new CustomLog("type", String.valueOf(i));
+ Mockito.verify(service, never()).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(log.toExtras()));
+ }
+ }
+
+ @Test(timeout = 3000L)
+ public void sendLog_always_returns_retriable_status_if_service_is_none() throws RemoteException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ CustomLog noIssue = new CustomLog("type", "noIssue");
+ CustomLog successAfterRetries = new CustomLog("type", "successAfterRetries");
+ CustomLog retryExceeded = new CustomLog("type", "retryExceeded");
+
+ doNothing().when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(noIssue.toExtras()));
+ doThrow(TransactionTooLargeException.class).doThrow(DeadObjectException.class).doNothing().when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(successAfterRetries.toExtras()));
+ doThrow(RemoteException.class).when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(retryExceeded.toExtras()));
+
+ for (int i = 0; i < 10; i++) {
+ Truth.assertThat(customLogInstructionSerializer.sendLog(noIssue)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(successAfterRetries)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(retryExceeded)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ }
+ }
+
+ @Test(timeout = 3000L)
+ public void sendLog_uses_retry_barrier() throws RemoteException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+ customLogInstructionSerializer.connect(service);
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).pause();
+
+ CustomLog noIssue = new CustomLog("type", "noIssue");
+ CustomLog successAfterRetries = new CustomLog("type", "successAfterRetries");
+ CustomLog retryExceeded = new CustomLog("type", "retryExceeded");
+
+ doNothing().when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(noIssue.toExtras()));
+ doThrow(TransactionTooLargeException.class).doThrow(DeadObjectException.class).doNothing().when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(successAfterRetries.toExtras()));
+ doThrow(RemoteException.class).when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(retryExceeded.toExtras()));
+
+ Truth.assertThat(customLogInstructionSerializer.sendLog(noIssue)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_SUCCESS);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(successAfterRetries)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(successAfterRetries)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(successAfterRetries)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_SUCCESS);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(retryExceeded)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(retryExceeded)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRIABLE);
+ Truth.assertThat(customLogInstructionSerializer.sendLog(retryExceeded)).isEqualTo(CustomLogInstructionSerializer.SEND_LOG_RESULT_FAILURE_RETRY_EXCEEDED);
+ }
+
+ @Test(timeout = 3000L)
+ public void requestSendingLog_works_regardless_of_service() throws RemoteException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ // Don't connect a service
+
+ for (int i = 0; i < 30; i++) {
+ customLogInstructionSerializer.requestSendingLog("type", "body");
+ }
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ Truth.assertThat(customLogInstructionSerializer.getPendingCount()).isEqualTo(30);
+ }
+
+ @Test(timeout = 3000L)
+ public void requestSendingLog_does_nothing_if_disabled() throws RemoteException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ customLogInstructionSerializer.setDisabled(true);
+
+ for (int i = 0; i < 30; i++) {
+ customLogInstructionSerializer.requestSendingLog("type", "body");
+ }
+
+ Truth.assertThat(customLogInstructionSerializer.hasHandlerInitialized()).isFalse();
+ Truth.assertThat(customLogInstructionSerializer.getPendingCount()).isEqualTo(0);
+
+ // Even if a service connection is established, this does nothing.
+ customLogInstructionSerializer.connect(service);
+
+ for (int i = 0; i < 30; i++) {
+ customLogInstructionSerializer.requestSendingLog("type", "body");
+ }
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ Truth.assertThat(customLogInstructionSerializer.getPendingCount()).isEqualTo(0);
+ }
+
+ @Test(timeout = 3000L)
+ public void retry_barrier_can_prevent_holding_logs_that_always_fail() throws RemoteException, InterruptedException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ doThrow(RemoteException.class).when(service).sendEvent(any(), any(), any());
+
+ customLogInstructionSerializer.connect(service);
+
+ for (int i = 0; i < 10; i++) {
+ CustomLog log = new CustomLog("type", String.valueOf(i));
+ customLogInstructionSerializer.requestSendingLog(log.type, log.body);
+ }
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ while (customLogInstructionSerializer.getPendingCount() > 0) {
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idleFor(100, TimeUnit.MILLISECONDS);
+ }
+
+ Truth.assertThat(customLogInstructionSerializer.getPendingCount()).isEqualTo(0);
+ Mockito.verify(service, times((CustomLogInstructionSerializer.MAX_RETRY_COUNT + 1) * 10)).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), any());
+ }
+
+ @Test(timeout = 3000L)
+ public void requestSendingLog_works_as_expected_with_retry_barrier() throws RemoteException {
+ CustomLogConfiguration configuration = new CustomLogConfiguration.Builder().build();
+ CustomLogInstructionSerializer customLogInstructionSerializer = new CustomLogInstructionSerializer(PACKAGE_NAME, configuration);
+
+ CustomLog noIssue = new CustomLog("type", "noIssue");
+ CustomLog successAfterRetries = new CustomLog("type", "successAfterRetries");
+ CustomLog retryExceeded = new CustomLog("type", "retryExceeded");
+
+ //noinspection unchecked
+ doThrow(TransactionTooLargeException.class, DeadObjectException.class).doCallRealMethod().when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(successAfterRetries.toExtras()));
+ doThrow(RemoteException.class).when(service).sendEvent(eq(PACKAGE_NAME), eq(DeployGateEvent.ACTION_SEND_CUSTOM_LOG), eq(retryExceeded.toExtras()));
+
+ customLogInstructionSerializer.connect(service);
+
+ customLogInstructionSerializer.requestSendingLog(successAfterRetries.type, successAfterRetries.body);
+ customLogInstructionSerializer.requestSendingLog(noIssue.type, noIssue.body);
+ customLogInstructionSerializer.requestSendingLog(retryExceeded.type, retryExceeded.body);
+
+ Shadows.shadowOf(customLogInstructionSerializer.getLooper()).idle();
+
+ List extras = service.getEventExtraList(DeployGateEvent.ACTION_SEND_CUSTOM_LOG);
+
+ Truth.assertThat(extras).hasSize(2);
+ BundleSubject.assertThat(extras.get(0)).isEqualTo(successAfterRetries.toExtras());
+ BundleSubject.assertThat(extras.get(1)).isEqualTo(noIssue.toExtras());
+ }
+}
diff --git a/sdk/src/test/java/com/deploygate/sdk/CustomLogTest.java b/sdk/src/test/java/com/deploygate/sdk/CustomLogTest.java
new file mode 100644
index 0000000..bc1391b
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/sdk/CustomLogTest.java
@@ -0,0 +1,31 @@
+package com.deploygate.sdk;
+
+import android.os.Bundle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.deploygate.sdk.truth.BundleSubject;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CustomLogTest {
+
+ @Test
+ public void CustomLog_toExtras_must_be_valid_format() {
+ CustomLog log = new CustomLog("error", "yes");
+
+ BundleSubject.assertThat(log.toExtras()).isEqualTo(createLogExtra("error", "yes"));
+ }
+
+ private static Bundle createLogExtra(
+ String type,
+ String body
+ ) {
+ Bundle bundle = new Bundle();
+ bundle.putString("logType", type);
+ bundle.putString("log", body);
+ return bundle;
+ }
+}
diff --git a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java
index 0505652..08956e8 100644
--- a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java
+++ b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java
@@ -90,6 +90,11 @@ public void install__Application_String_DeployGateCallback_boolean() {
DeployGate.install(app, "author", callback, true);
}
+ @Test
+ public void install__Application_String_DeployGateCallback_CustomLogConfiguration() {
+ DeployGate.install(app, "author", callback, true, new CustomLogConfiguration.Builder().build());
+ }
+
@Test
public void refresh() {
DeployGate.refresh();
diff --git a/sdk/src/test/java/com/deploygate/sdk/helper/Bundles.java b/sdk/src/test/java/com/deploygate/sdk/helper/Bundles.java
new file mode 100644
index 0000000..4f74555
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/sdk/helper/Bundles.java
@@ -0,0 +1,28 @@
+package com.deploygate.sdk.helper;
+
+import android.os.Bundle;
+
+import java.util.Objects;
+
+public class Bundles {
+ public static boolean equals(
+ Bundle left,
+ Bundle right
+ ) {
+ if (left.size() != right.size()) {
+ return false;
+ }
+
+ if (!left.keySet().equals(right.keySet())) {
+ return false;
+ }
+
+ for (final String key : left.keySet()) {
+ if (!Objects.equals(left.get(key), right.get(key))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java b/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java
new file mode 100644
index 0000000..a9f8771
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java
@@ -0,0 +1,34 @@
+package com.deploygate.sdk.mockito;
+
+import android.os.Bundle;
+
+import com.deploygate.sdk.helper.Bundles;
+
+import org.mockito.ArgumentMatcher;
+
+import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
+
+public class BundleMatcher {
+ public static Bundle eq(Bundle expected) {
+ mockingProgress().getArgumentMatcherStorage().reportMatcher(new Equals(expected));
+ return expected;
+ }
+
+ private static class Equals implements ArgumentMatcher {
+ private final Bundle expected;
+
+ public Equals(Bundle expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean matches(Bundle argument) {
+ return Bundles.equals(expected, argument);
+ }
+
+ @Override
+ public String toString() {
+ return expected.toString();
+ }
+ }
+}
diff --git a/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java b/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java
new file mode 100644
index 0000000..0e8402b
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java
@@ -0,0 +1,61 @@
+package com.deploygate.sdk.truth;
+
+import android.os.Bundle;
+
+import com.deploygate.sdk.helper.Bundles;
+import com.google.common.truth.Fact;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+import java.util.Locale;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+public class BundleSubject extends Subject {
+ public static Factory bundles() {
+ return new Factory() {
+ @Override
+ public BundleSubject createSubject(
+ FailureMetadata metadata,
+ Bundle actual
+ ) {
+ return new BundleSubject(metadata, actual);
+ }
+ };
+ }
+
+ public static BundleSubject assertThat(Bundle actual) {
+ return assertAbout(bundles()).that(actual);
+ }
+
+ private final Bundle actual;
+
+ /**
+ * Constructor for use by subclasses. If you want to create an instance of this class itself, call
+ * {@link Subject#check(String, Object ..) check(...)}{@code .that(actual)}.
+ *
+ * @param metadata
+ * @param actual
+ */
+ protected BundleSubject(
+ FailureMetadata metadata,
+ Bundle actual
+ ) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ @Override
+ public void isEqualTo(Object expected) {
+ if (!Bundles.equals((Bundle) expected, actual)) {
+ failWithActual(Fact.simpleFact(String.format(Locale.US, "%s to be same to %s", expected.toString(), actual.toString())));
+ }
+ }
+
+ @Override
+ public void isNotEqualTo(Object unexpected) {
+ if (Bundles.equals((Bundle) unexpected, actual)) {
+ failWithActual(Fact.simpleFact(String.format(Locale.US, "%s to unexpectedly be same to %s", unexpected.toString(), actual.toString())));
+ }
+ }
+}
diff --git a/sdk/src/test/java/com/deploygate/service/FakeDeployGateClientService.java b/sdk/src/test/java/com/deploygate/service/FakeDeployGateClientService.java
new file mode 100644
index 0000000..d2cf8ec
--- /dev/null
+++ b/sdk/src/test/java/com/deploygate/service/FakeDeployGateClientService.java
@@ -0,0 +1,79 @@
+package com.deploygate.service;
+
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.google.common.truth.Truth;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public class FakeDeployGateClientService implements IDeployGateSdkService {
+ public static final String ACTION_INIT = "com.deploygate.sdk.fake.INIT";
+
+ private final String packageName;
+ private final Map> eventExtrasMap;
+
+ public FakeDeployGateClientService(
+ String packageName
+ ) {
+ this.packageName = packageName;
+ this.eventExtrasMap = new HashMap<>();
+
+ for (final Field field : DeployGateEvent.class.getFields()) {
+ final String name = field.getName();
+
+ if (name.startsWith("ACTION_")) {
+ try {
+ eventExtrasMap.put((String) field.get(null), new ArrayList<>());
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(String.format(Locale.US, "cannot access %s", name), e);
+ }
+ }
+ }
+ }
+
+ public List getEventExtraList(String action) {
+ return Collections.unmodifiableList(eventExtrasMap.getOrDefault(action, new ArrayList<>()));
+ }
+
+ @Override
+ public void init(
+ IDeployGateSdkServiceCallback callback,
+ String packageName,
+ Bundle extras
+ ) throws RemoteException {
+ Truth.assertThat(packageName).isEqualTo(this.packageName);
+ recordEvent(ACTION_INIT, extras);
+ }
+
+ @Override
+ public void sendEvent(
+ String packageName,
+ String action,
+ Bundle extras
+ ) throws RemoteException {
+ Truth.assertThat(packageName).isEqualTo(this.packageName);
+ recordEvent(action, extras);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ private void recordEvent(
+ String action,
+ Bundle extras
+ ) {
+ List extraList = Objects.requireNonNull(eventExtrasMap.get(action), String.format(Locale.US, "%s is an unknown action", action));
+ extraList.add(extras);
+ }
+}
diff --git a/sdkMock/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java b/sdkMock/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java
new file mode 100644
index 0000000..2978d90
--- /dev/null
+++ b/sdkMock/src/main/java/com/deploygate/sdk/CustomLogConfiguration.java
@@ -0,0 +1,51 @@
+package com.deploygate.sdk;
+
+public class CustomLogConfiguration {
+ public enum Backpressure {
+ /**
+ * SDK rejects new logs if buffer size is exceeded
+ */
+ PRESERVE_BUFFER,
+
+ /**
+ * SDK drops logs from the oldest if buffer size is exceeded
+ */
+ DROP_BUFFER_BY_OLDEST
+ }
+
+ private CustomLogConfiguration(
+ ) {
+ }
+
+ public static class Builder {
+ /**
+ * @param backpressure
+ * the strategy of the backpressure in the log buffer
+ *
+ * @return self
+ *
+ * @see Backpressure
+ */
+ public Builder setBackpressure(Backpressure backpressure) {
+ if (backpressure == null) {
+ throw new IllegalArgumentException("backpressure must be non-null");
+ }
+
+ return this;
+ }
+
+ /**
+ * @param bufferSize
+ * the max size of the log buffer
+ *
+ * @return self
+ */
+ public Builder setBufferSize(int bufferSize) {
+ return this;
+ }
+
+ public CustomLogConfiguration build() {
+ return new CustomLogConfiguration();
+ }
+ }
+}
diff --git a/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java b/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java
index 976d778..be7867f 100644
--- a/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java
+++ b/sdkMock/src/main/java/com/deploygate/sdk/DeployGate.java
@@ -50,6 +50,15 @@ public static void install(
) {
}
+ public static void install(
+ Application app,
+ String author,
+ DeployGateCallback callback,
+ boolean forceApplyOnReleaseBuild,
+ CustomLogConfiguration configuration
+ ) {
+ }
+
public static void refresh() {
}