diff --git a/README.md b/README.md index 463371d..d4c2cfd 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,14 @@ Unless required by applicable law or agreed to in writing, software distributed - Add new tests for your changes - Make sure all tests are passed +```bash +# help: build and install artifacts into your local maven repo +./gradlew clean \ + sdk:verifyBytecodeVersionRelease sdkMock:verifyBytecodeVersionRelease \ + sdk:publishReleasePublicationToMavenLocal sdkMock:publishReleasePublicationToMavenLocal \ + --stacktrace +``` + ### sdk - Consider if we should use external libraries carefully diff --git a/build.gradle b/build.gradle index fd93ca5..50fcc62 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. ext { - releaseVersion = '4.5.0' + releaseVersion = '4.6.0-alpha03' } buildscript { diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..2837436 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,5 @@ +jdk: + - openjdk17 +before_install: + - sdk install java 17.0.8-tem + - sdk use java 17.0.8-tem diff --git a/sdk.build.gradle b/sdk.build.gradle index 6dab261..5a31a42 100644 --- a/sdk.build.gradle +++ b/sdk.build.gradle @@ -11,7 +11,34 @@ android { targetSdkVersion 33 versionCode 1 versionName project.version - manifestPlaceholders = [sdkVersion:"4"] + + // A map of name to isSupporting + def features = [ + UPDATE_MESSAGE_OF_BUILD: true, + SERIALIZED_EXCEPTION: true, + LOGCAT_BUNDLE: true, + STREAMED_LOGCAT: true, + DEVICE_CAPTURE: true, + ] + + if (!(features instanceof LinkedHashMap)) { + throw new IllegalAccessException("The key order may not be kept") + } + + int flags = 0 + + features.keySet().eachWithIndex { String key, int i -> + buildConfigField("int", key, "1 << $i") + + if (features[key]) { + flags |= 1 << i + } + } + + manifestPlaceholders += [ + featureFlags: flags, + sdkVersion: "4" + ] } buildTypes { @@ -37,6 +64,10 @@ android { jvmArgs "-Xmx1g" } } + + buildFeatures { + buildConfig = true + } } dependencies { diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml index 5491859..914a0b1 100644 --- a/sdk/src/main/AndroidManifest.xml +++ b/sdk/src/main/AndroidManifest.xml @@ -13,6 +13,11 @@ android:name="com.deploygate.sdk.version" android:value="${sdkVersion}" /> + + diff --git a/sdk/src/main/java/com/deploygate/sdk/Compatibility.java b/sdk/src/main/java/com/deploygate/sdk/Compatibility.java index 723cc47..f60c075 100644 --- a/sdk/src/main/java/com/deploygate/sdk/Compatibility.java +++ b/sdk/src/main/java/com/deploygate/sdk/Compatibility.java @@ -1,10 +1,17 @@ package com.deploygate.sdk; +/** + * A metadata content to represent feature compatibilities of the client app. + * + * NOTE: Do not remove any enum entry even if it's completely removed from the client app. + */ enum Compatibility { - UPDATE_MESSAGE_OF_BUILD(1), - SERIALIZED_EXCEPTION(1 << 1), - LOGCAT_BUNDLE(1 << 2), - STREAMED_LOGCAT(1 << 3); + UPDATE_MESSAGE_OF_BUILD(BuildConfig.UPDATE_MESSAGE_OF_BUILD), + SERIALIZED_EXCEPTION(BuildConfig.SERIALIZED_EXCEPTION), + LOGCAT_BUNDLE(BuildConfig.LOGCAT_BUNDLE), + STREAMED_LOGCAT(BuildConfig.STREAMED_LOGCAT), + + DEVICE_CAPTURE(BuildConfig.DEVICE_CAPTURE); final int bitMask; diff --git a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java index 1282299..9265236 100644 --- a/sdk/src/main/java/com/deploygate/sdk/DeployGate.java +++ b/sdk/src/main/java/com/deploygate/sdk/DeployGate.java @@ -16,13 +16,18 @@ import android.util.Log; import com.deploygate.sdk.internal.Logger; +import com.deploygate.sdk.internal.VisibilityLifecycleCallbacks; import com.deploygate.service.DeployGateEvent; import com.deploygate.service.IDeployGateSdkService; import com.deploygate.service.IDeployGateSdkServiceCallback; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; +import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * This is DeployGate SDK library. Import this library to the application @@ -42,6 +47,7 @@ public class DeployGate { private static final String ACTION_DEPLOYGATE_STARTED = "com.deploygate.action.ServiceStarted"; private static final String DEPLOYGATE_PACKAGE = "com.deploygate"; + private static final Object sPendingEventLock = new Object(); private static DeployGate sInstance; @@ -52,6 +58,7 @@ public class DeployGate { private final ILogcatInstructionSerializer mLogcatInstructionSerializer; private final CustomLogInstructionSerializer mCustomLogInstructionSerializer; private final HashSet mCallbacks; + private final HashMap mPendingEvents; private final String mExpectedAuthor; private String mAuthor; @@ -85,12 +92,22 @@ public void onEvent( return; } + // ensure non-null + extras = extras != null ? extras : new Bundle(); + if (DeployGateEvent.ACTION_INIT.equals(action)) { onInitialized(extras.getBoolean(DeployGateEvent.EXTRA_IS_MANAGED, false), extras.getBoolean(DeployGateEvent.EXTRA_IS_AUTHORIZED, false), extras.getString(DeployGateEvent.EXTRA_LOGIN_USERNAME), extras.getString(DeployGateEvent.EXTRA_DISTRIBUTION_USER_NAME), extras.getBoolean(DeployGateEvent.EXTRA_IS_STOP_REQUESTED, false), extras.getString(DeployGateEvent.EXTRA_AUTHOR), extras.getInt(DeployGateEvent.EXTRA_CURRENT_REVISION, 0), extras.getString(DeployGateEvent.EXTRA_CURRENT_DISTRIBUTION_ID), extras.getString(DeployGateEvent.EXTRA_CURRENT_DISTRIBUTION_TITLE)); } else if (DeployGateEvent.ACTION_UPDATE_AVAILABLE.equals(action)) { onUpdateArrived(extras.getInt(DeployGateEvent.EXTRA_SERIAL), extras.getString(DeployGateEvent.EXTRA_VERSION_NAME), extras.getInt(DeployGateEvent.EXTRA_VERSION_CODE), extras.getString(DeployGateEvent.EXTRA_SERIAL_MESSAGE)); } else if (DeployGateEvent.ACTION_ONESHOT_LOGCAT.equals(action)) { - onOneshotLogcat(); + String captureId = null; + + if (mDeployGateClient.isSupported(Compatibility.DEVICE_CAPTURE)) { + // still nullable + captureId = extras.getString(DeployGateEvent.EXTRA_CAPTURE_ID); + } + + onOneshotLogcat(captureId); } else if (DeployGateEvent.ACTION_ENABLE_LOGCAT.equals(action)) { if (mDeployGateClient.isSupported(Compatibility.STREAMED_LOGCAT)) { String sessionKey = extras.getString(DeployGateEvent.EXTRA_LOGCAT_STREAM_SESSION_KEY); @@ -110,6 +127,8 @@ public void onEvent( } else { Logger.w("streamed logcat is not supported"); } + } else { + Logger.w("%s is not supported by this sdk version", action); } } @@ -153,6 +172,9 @@ public void run() { mLogcatInstructionSerializer.connect(mRemoteService); mInitializedLatch.countDown(); + + // to release a lock as soon as possible. + flushPendingEvents(); } private void onUpdateArrived( @@ -178,12 +200,38 @@ public void run() { } }; + @SuppressWarnings("FieldCanBeLocal") + private final VisibilityLifecycleCallbacks.OnVisibilityChangeListener mOnVisibilityChangeListener = new VisibilityLifecycleCallbacks.OnVisibilityChangeListener() { + + @Override + public void onForeground( + long elapsedRealtime, + TimeUnit timeUnit + ) { + Bundle extras = new Bundle(); + extras.putLong(DeployGateEvent.EXTRA_VISIBILITY_EVENT_ELAPSED_REAL_TIME_IN_NANOS, timeUnit.toNanos(elapsedRealtime)); + extras.putInt(DeployGateEvent.EXTRA_VISIBILITY_EVENT_TYPE, DeployGateEvent.VisibilityType.FOREGROUND); + invokeAction(DeployGateEvent.ACTION_VISIBILITY_EVENT, extras, true); + } + + @Override + public void onBackground( + long elapsedRealtime, + TimeUnit timeUnit + ) { + Bundle extras = new Bundle(); + extras.putLong(DeployGateEvent.EXTRA_VISIBILITY_EVENT_ELAPSED_REAL_TIME_IN_NANOS, timeUnit.toNanos(elapsedRealtime)); + extras.putInt(DeployGateEvent.EXTRA_VISIBILITY_EVENT_TYPE, DeployGateEvent.VisibilityType.BACKGROUND); + invokeAction(DeployGateEvent.ACTION_VISIBILITY_EVENT, extras, true); + } + }; + private void requestOneshotLogcat() { - onOneshotLogcat(); + onOneshotLogcat(null); } - private void onOneshotLogcat() { - mLogcatInstructionSerializer.requestOneshotLogcat(); + private void onOneshotLogcat(String captureId) { + mLogcatInstructionSerializer.requestOneshotLogcat(captureId); } private void onEnableStreamedLogcat(String streamSessionKey) { @@ -229,7 +277,8 @@ private DeployGate( mHandler = new Handler(); mLogcatInstructionSerializer = mHostApp.canUseLogcat ? new LogcatInstructionSerializer(mHostApp.packageName) : ILogcatInstructionSerializer.NULL_INSTANCE; mCustomLogInstructionSerializer = new CustomLogInstructionSerializer(mHostApp.packageName, customLogConfiguration); - mCallbacks = new HashSet(); + mCallbacks = new HashSet<>(); + mPendingEvents = new HashMap<>(); mExpectedAuthor = author; prepareBroadcastReceiver(); @@ -239,6 +288,7 @@ private DeployGate( } mInitializedLatch = new CountDownLatch(1); + ((Application) applicationContext).registerActivityLifecycleCallbacks(new VisibilityLifecycleCallbacks(mOnVisibilityChangeListener)); initService(true); } @@ -318,11 +368,30 @@ private void requestServiceInit(final boolean isBoot) { } } + /** + * Send an event to the client application + * + * @param action + * to be sent + * @param extras + * to be sent + * @param allowPending + * Allow queueing events to send them after a service connection is established (since 4.6.0) + */ private void invokeAction( String action, - Bundle extras + Bundle extras, + boolean allowPending ) { + extras = extras != null ? extras : new Bundle(); + if (mRemoteService == null) { + if (allowPending) { + synchronized (sPendingEventLock) { + mPendingEvents.put(action, extras); + } + } + return; } try { @@ -332,6 +401,19 @@ private void invokeAction( } } + private void flushPendingEvents() { + // cannot re-enqueue events for now + synchronized (sPendingEventLock) { + Iterator> iterator = mPendingEvents.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + invokeAction(entry.getKey(), entry.getValue(), false); + iterator.remove(); + } + } + } + /** * Clear the initiated DeployGate instance. *

@@ -1191,7 +1273,7 @@ public static void installUpdate() { return; } - sInstance.invokeAction(DeployGateEvent.ACTION_INSTALL_UPDATE, null); + sInstance.invokeAction(DeployGateEvent.ACTION_INSTALL_UPDATE, null, false); } /** @@ -1207,7 +1289,7 @@ public static void openComments() { return; } - sInstance.invokeAction(DeployGateEvent.ACTION_OPEN_COMMENTS, null); + sInstance.invokeAction(DeployGateEvent.ACTION_OPEN_COMMENTS, null, false); } /** @@ -1240,7 +1322,7 @@ public static void composeComment(String defaultComment) { Bundle extras = new Bundle(); extras.putString(DeployGateEvent.EXTRA_COMMENT, defaultComment); - sInstance.invokeAction(DeployGateEvent.ACTION_COMPOSE_COMMENT, extras); + sInstance.invokeAction(DeployGateEvent.ACTION_COMPOSE_COMMENT, extras, false); } /** diff --git a/sdk/src/main/java/com/deploygate/sdk/ILogcatInstructionSerializer.java b/sdk/src/main/java/com/deploygate/sdk/ILogcatInstructionSerializer.java index c348ba5..2302724 100644 --- a/sdk/src/main/java/com/deploygate/sdk/ILogcatInstructionSerializer.java +++ b/sdk/src/main/java/com/deploygate/sdk/ILogcatInstructionSerializer.java @@ -20,8 +20,11 @@ interface ILogcatInstructionSerializer { /** * Create and enqueue a request to start sending oneshot logcat + * + * @param captureId + * this is nullable. Set to non-null if this logcat is for a capture. */ - boolean requestOneshotLogcat(); + boolean requestOneshotLogcat(String captureId); /** * Create and enqueue a request to start sending streamed logcat @@ -56,8 +59,8 @@ public void disconnect() { } @Override - public boolean requestOneshotLogcat() { - Logger.d("Logcat (no-op): requestOneshotLogcat"); + public boolean requestOneshotLogcat(String captureId) { + Logger.d("Logcat (no-op): requestOneshotLogcat(%s)", captureId != null ? captureId : "null"); return false; } diff --git a/sdk/src/main/java/com/deploygate/sdk/LogcatInstructionSerializer.java b/sdk/src/main/java/com/deploygate/sdk/LogcatInstructionSerializer.java index c81ed6e..fd4dc45 100644 --- a/sdk/src/main/java/com/deploygate/sdk/LogcatInstructionSerializer.java +++ b/sdk/src/main/java/com/deploygate/sdk/LogcatInstructionSerializer.java @@ -67,21 +67,25 @@ class LogcatInstructionSerializer implements ILogcatInstructionSerializer { this.logcatProcess = new LogcatProcess(new LogcatProcess.Callback() { @Override public void onStarted(String processId) { + //noinspection ConstantConditions handler.enqueueSendLogcatMessageInstruction(SendLogcatRequest.createBeginning(processId)); } @Override public void emit( String processId, - ArrayList logcatLines + ArrayList logcatLines, + String captureId ) { ensureHandlerPrepared(); - handler.enqueueSendLogcatMessageInstruction(new SendLogcatRequest(processId, logcatLines)); + //noinspection ConstantConditions + handler.enqueueSendLogcatMessageInstruction(new SendLogcatRequest(processId, logcatLines, captureId)); } @Override public void onFinished(String processId) { + //noinspection ConstantConditions handler.enqueueSendLogcatMessageInstruction(SendLogcatRequest.createTermination(processId)); } }); @@ -91,6 +95,7 @@ public void onFinished(String processId) { @Override public final synchronized void connect(IDeployGateSdkService service) { + //noinspection ConstantConditions if (service == null) { throw new IllegalArgumentException("service must not be null"); } @@ -107,8 +112,8 @@ public final void disconnect() { } @Override - public final synchronized boolean requestOneshotLogcat() { - return requestLogcat(null); + public final synchronized boolean requestOneshotLogcat(String captureId) { + return requestLogcat(null, captureId); } @Override @@ -136,7 +141,10 @@ public final void setEnabled(boolean enabled) { public final void stopStream() { ensureHandlerPrepared(); - logcatProcess.stop(); + if (logcatProcess != null) { + logcatProcess.stop(); + } + //noinspection ConstantConditions handler.cancelPendingSendLogcatInstruction(); } @@ -216,13 +224,28 @@ int sendSingleChunk( * @return true if new process has lauched */ private boolean requestLogcat(String streamSessionKey) { + return requestLogcat(streamSessionKey, null); + } + + /** + * @param streamSessionKey + * nullable. sdk can not generate this key. + * @param captureId + * nullable. + * + * @return true if new process has lauched + */ + private boolean requestLogcat( + String streamSessionKey, + String captureId + ) { ensureHandlerPrepared(); - if (!isEnabled) { + if (!isEnabled || logcatProcess == null) { return false; } - Pair ids = logcatProcess.execute(streamSessionKey); + Pair ids = logcatProcess.execute(streamSessionKey, captureId); String retiredId = ids.first; String newId = ids.second; @@ -234,6 +257,7 @@ private boolean requestLogcat(String streamSessionKey) { if (!LogcatProcess.UNKNOWN_PROCESS_ID.equals(retiredId)) { // the previous on-going execution has been retied + //noinspection ConstantConditions handler.cancelPendingSendLogcatInstruction(retiredId); } @@ -287,10 +311,11 @@ private int sendChunkedLogcats( */ /** - * @return the handler instance or null if not prepared. + * @return the handler instance */ Handler getHandler() { ensureHandlerPrepared(); + //noinspection ConstantConditions return handler; } @@ -327,8 +352,6 @@ private static class LogcatHandler extends Handler { /** * Cancel the send-logcat instruction of all watchers in the handler message queue. - * - * @return true if canceled, otherwise false. */ void cancelPendingSendLogcatInstruction() { synchronized (requestMap) { @@ -341,8 +364,6 @@ void cancelPendingSendLogcatInstruction() { /** * Cancel the send-logcat instruction of the specific watcher in the handler message queue. - * - * @return true if canceled, otherwise false. */ void cancelPendingSendLogcatInstruction(String bundleId) { acquireRequests(bundleId); diff --git a/sdk/src/main/java/com/deploygate/sdk/LogcatProcess.java b/sdk/src/main/java/com/deploygate/sdk/LogcatProcess.java index 994c9f9..2d272ef 100644 --- a/sdk/src/main/java/com/deploygate/sdk/LogcatProcess.java +++ b/sdk/src/main/java/com/deploygate/sdk/LogcatProcess.java @@ -27,7 +27,8 @@ interface Callback { void emit( String processId, - ArrayList logcatLines + ArrayList logcatLines, + String captureId ); void onFinished(String processId); @@ -58,7 +59,8 @@ void emit( * @return a pair of watcher ids (non-nulls). first is the previous watcher id, second is the new watcher id. */ Pair execute( - @Experimental String streamSessionKey + @Experimental String streamSessionKey, + String captureId ) { Pair ids; @@ -77,7 +79,7 @@ Pair execute( return Pair.create(currentPid, currentPid); } - final LogcatWatcher newWatcher = new LogcatWatcher(streamSessionKey, callback); + final LogcatWatcher newWatcher = new LogcatWatcher(streamSessionKey, captureId, callback); try { this.latestLogcatWatcher = newWatcher; @@ -121,16 +123,23 @@ static class LogcatWatcher implements Runnable { private final String processId; private final boolean isOneShot; + private final String captureId; private final WeakReference callback; private final AtomicReference processRef; private final AtomicInteger state; LogcatWatcher( @Experimental String streamSessionKey, + String captureId, Callback callback ) { + if (streamSessionKey != null && captureId != null) { + throw new IllegalArgumentException("streaming and capture cannot be specified at once"); + } + this.processId = streamSessionKey != null ? streamSessionKey : ClientId.generate(); this.isOneShot = streamSessionKey == null; + this.captureId = captureId; this.callback = new WeakReference<>(callback); this.processRef = new AtomicReference<>(); this.state = new AtomicInteger(STATE_READY); @@ -228,10 +237,10 @@ public void run() { if (isOneShot) { continue; } else if (logcatBuf.size() >= MAX_LINES) { - callback.emit(processId, toArrayList(logcatBuf)); + callback.emit(processId, toArrayList(logcatBuf), captureId); logcatBuf = createBuffer(MAX_LINES); // Don't reuse to make sure releasing the reference } else if (!bufferedReader.ready()) { - callback.emit(processId, toArrayList(logcatBuf)); + callback.emit(processId, toArrayList(logcatBuf), captureId); logcatBuf = createBuffer(MAX_LINES); // Don't reuse to make sure releasing the reference } else { continue; @@ -248,7 +257,7 @@ public void run() { Callback callback = this.callback.get(); if (callback != null) { - callback.emit(processId, toArrayList(logcatBuf)); + callback.emit(processId, toArrayList(logcatBuf), captureId); } } diff --git a/sdk/src/main/java/com/deploygate/sdk/SendLogcatRequest.java b/sdk/src/main/java/com/deploygate/sdk/SendLogcatRequest.java index 3050348..f0eed77 100644 --- a/sdk/src/main/java/com/deploygate/sdk/SendLogcatRequest.java +++ b/sdk/src/main/java/com/deploygate/sdk/SendLogcatRequest.java @@ -31,22 +31,24 @@ private String label() { } public static SendLogcatRequest createTermination(String processId) { - return new SendLogcatRequest(processId, new ArrayList(), Position.Termination); + return new SendLogcatRequest(processId, new ArrayList(), Position.Termination, null); } public static SendLogcatRequest createBeginning(String processId) { - return new SendLogcatRequest(processId, new ArrayList(), Position.Beginning); + return new SendLogcatRequest(processId, new ArrayList(), Position.Beginning, null); } public final ArrayList lines; public final Position position; + public final String captureId; private int retryCount; SendLogcatRequest( String pid, - List lines + List lines, + String captureId ) { - this(pid, lines, Position.Content); + this(pid, lines, Position.Content, captureId); } /** @@ -56,15 +58,19 @@ public static SendLogcatRequest createBeginning(String processId) { * logcat contents if available. Zero value is an empty list. * @param position * a position of this request. non-null + * @param captureId + * the id of the capture. nullable */ private SendLogcatRequest( String pid, List lines, - Position position + Position position, + String captureId ) { super(pid); this.lines = lines instanceof ArrayList ? (ArrayList) lines : new ArrayList<>(lines); this.position = position; + this.captureId = captureId; } /** @@ -95,7 +101,7 @@ List splitInto(int count) { for (int i = 0, offset = 0, step = size / count; i < count; i++, offset += step) { final int endIndex = (i == count - 1) ? size : offset + step; - splits.add(new SendLogcatRequest(gid, lines.subList(offset, endIndex), Position.Content)); + splits.add(new SendLogcatRequest(gid, lines.subList(offset, endIndex), Position.Content, captureId)); } return splits; @@ -105,5 +111,8 @@ List splitInto(int count) { void applyValues(Bundle extras) { extras.putStringArrayList(DeployGateEvent.EXTRA_LOG, lines); extras.putString(DeployGateEvent.EXTRA_BUNDLE_POSITION, position.label()); + if (captureId != null) { + extras.putString(DeployGateEvent.EXTRA_CAPTURE_ID, captureId); + } } } diff --git a/sdk/src/main/java/com/deploygate/sdk/internal/VisibilityLifecycleCallbacks.java b/sdk/src/main/java/com/deploygate/sdk/internal/VisibilityLifecycleCallbacks.java new file mode 100644 index 0000000..3a21a56 --- /dev/null +++ b/sdk/src/main/java/com/deploygate/sdk/internal/VisibilityLifecycleCallbacks.java @@ -0,0 +1,86 @@ +package com.deploygate.sdk.internal; + +import android.app.Activity; +import android.app.Application; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemClock; + +import java.util.concurrent.TimeUnit; + +public final class VisibilityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { + public interface OnVisibilityChangeListener { + void onForeground( + long elapsedRealtime, + TimeUnit timeUnit + ); + + void onBackground( + long elapsedRealtime, + TimeUnit timeUnit + ); + } + + private int onResumeCount = 0; // this is manipulated from the single thread + + private final OnVisibilityChangeListener listener; + + public VisibilityLifecycleCallbacks(OnVisibilityChangeListener listener) { + this.listener = listener; + } + + @Override + public void onActivityCreated( + Activity activity, + Bundle savedInstanceState + ) { + // no-op + } + + @Override + public void onActivityStarted(Activity activity) { + // no-op + } + + @Override + public void onActivityResumed(Activity activity) { + onResumeCount++; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + listener.onForeground(SystemClock.elapsedRealtimeNanos(), TimeUnit.NANOSECONDS); + } else { + listener.onForeground(SystemClock.elapsedRealtime(), TimeUnit.MILLISECONDS); + } + } + + @Override + public void onActivityPaused(Activity activity) { + onResumeCount = Math.max(onResumeCount - 1, 0); // cuz uint is unavailable. + + if (onResumeCount == 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + listener.onBackground(SystemClock.elapsedRealtimeNanos(), TimeUnit.NANOSECONDS); + } else { + listener.onBackground(SystemClock.elapsedRealtime(), TimeUnit.MILLISECONDS); + } + } + } + + @Override + public void onActivityStopped(Activity activity) { + // no-op + } + + @Override + public void onActivitySaveInstanceState( + Activity activity, + Bundle outState + ) { + // no-op + } + + @Override + public void onActivityDestroyed(Activity activity) { + // no-op + } +} diff --git a/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java b/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java index d0d29dd..f188c50 100644 --- a/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java +++ b/sdk/src/main/java/com/deploygate/service/DeployGateEvent.java @@ -25,6 +25,7 @@ public interface DeployGateEvent { public static final String ACTION_OPEN_APP_DETAIL = "openAppDetail"; public static final String ACTION_OPEN_COMMENTS = "openComments"; public static final String ACTION_COMPOSE_COMMENT = "composeComment"; + public static final String ACTION_VISIBILITY_EVENT = "a.visibility-event"; public static final String EXTRA_AUTHOR = "author"; public static final String EXTRA_EXPECTED_AUTHOR = "expectedAuthor"; @@ -85,4 +86,25 @@ public interface DeployGateEvent { public static final String EXTRA_DEPLOYGATE_VERSION_CODE = "deploygateVersionCode"; public static final String EXTRA_COMMENT = "comment"; public static final String EXTRA_DISTRIBUTION_USER_NAME = "distributionUserName"; + + /** + * the id of the capture. + */ + public static final String EXTRA_CAPTURE_ID = "e.capture-id"; + + /** + * A event type for the app goes to foreground/background. + */ + public static final String EXTRA_VISIBILITY_EVENT_TYPE = "e.visibility-event-type"; + + /** + * The elapsed real time since boot at the time when the app goes to foreground/background. + * this value must be nano times. + */ + public static final String EXTRA_VISIBILITY_EVENT_ELAPSED_REAL_TIME_IN_NANOS = "e.visibility-event-elapsed-real-time"; + + interface VisibilityType { + int BACKGROUND = 0; + int FOREGROUND = 1; + } } diff --git a/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java b/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java index 0c555fc..7bb8228 100644 --- a/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/CustomLogInstructionSerializerTest.java @@ -1,5 +1,15 @@ package com.deploygate.sdk; +import static com.deploygate.sdk.mockito.BundleMatcher.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +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; + import android.os.Bundle; import android.os.DeadObjectException; import android.os.RemoteException; @@ -26,16 +36,6 @@ 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.anyString; -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 { diff --git a/sdk/src/test/java/com/deploygate/sdk/DeployGateClientTest.java b/sdk/src/test/java/com/deploygate/sdk/DeployGateClientTest.java index ccea880..fb4d016 100644 --- a/sdk/src/test/java/com/deploygate/sdk/DeployGateClientTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/DeployGateClientTest.java @@ -1,5 +1,7 @@ package com.deploygate.sdk; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + import android.app.Application; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -21,8 +23,6 @@ import org.robolectric.Shadows; import org.robolectric.shadows.ShadowPackageManager; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - @RunWith(AndroidJUnit4.class) public class DeployGateClientTest { @NonNull diff --git a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java index e2349f9..e5d4e15 100644 --- a/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/DeployGateInterfaceTest.java @@ -1,5 +1,7 @@ package com.deploygate.sdk; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + import android.app.Application; import androidx.annotation.NonNull; @@ -11,8 +13,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - /** * This test class will make sure all *public* interfaces are defined as expected diff --git a/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java b/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java index 4686656..afc8e39 100644 --- a/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/HostAppTest.java @@ -1,5 +1,7 @@ package com.deploygate.sdk; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + import android.content.Context; import androidx.annotation.NonNull; @@ -12,8 +14,6 @@ import org.junit.runner.RunWith; import org.robolectric.annotation.Config; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - @RunWith(AndroidJUnit4.class) public class HostAppTest { @NonNull diff --git a/sdk/src/test/java/com/deploygate/sdk/LogcatInstructionSerializerTest.java b/sdk/src/test/java/com/deploygate/sdk/LogcatInstructionSerializerTest.java index 1d6667d..25a99d0 100644 --- a/sdk/src/test/java/com/deploygate/sdk/LogcatInstructionSerializerTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/LogcatInstructionSerializerTest.java @@ -1,5 +1,15 @@ package com.deploygate.sdk; +import static com.deploygate.sdk.mockito.BundleMatcher.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mockStatic; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; + import android.os.Bundle; import android.os.RemoteException; import android.os.TransactionTooLargeException; @@ -7,7 +17,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.deploygate.sdk.helper.FakeLogcat; -import com.deploygate.sdk.mockito.BundleMatcher; import com.deploygate.service.DeployGateEvent; import com.deploygate.service.FakeDeployGateClientService; import com.google.common.truth.Truth; @@ -29,17 +38,6 @@ import java.util.List; import java.util.Random; -import static com.deploygate.sdk.mockito.BundleMatcher.eq; -import static com.deploygate.sdk.mockito.BundleMatcher.eq; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mockStatic; -import static org.robolectric.annotation.LooperMode.Mode.PAUSED; - @RunWith(AndroidJUnit4.class) @LooperMode(PAUSED) public class LogcatInstructionSerializerTest { @@ -87,7 +85,7 @@ public void after() { instructionSerializer.halt(); } - for (Process process: processes) { + for (Process process : processes) { if (process.isAlive()) { process.destroy(); } @@ -102,11 +100,30 @@ public void after() { public void sendSingleChunk_always_returns_retriable_status_if_service_is_none() throws RemoteException { instructionSerializer = new LogcatInstructionSerializer(PACKAGE_NAME); - instructionSerializer.requestOneshotLogcat(); + instructionSerializer.requestOneshotLogcat(null); + + SendLogcatRequest chunk1 = new SendLogcatRequest("tid1", new ArrayList<>(Arrays.asList("line1", "line2", "line3")), null); + SendLogcatRequest chunk2 = new SendLogcatRequest("tid2", new ArrayList<>(Arrays.asList("line4", "line5", "line6")), null); + SendLogcatRequest chunk3 = new SendLogcatRequest("tid3", new ArrayList<>(Arrays.asList("line7", "line8", "line9")), null); + + doNothing().when(service).sendEvent(anyString(), anyString(), any(Bundle.class)); + + Truth.assertThat(instructionSerializer.sendSingleChunk(chunk1)).isEqualTo(LogcatInstructionSerializer.SEND_LOGCAT_RESULT_FAILURE_RETRIABLE); + Truth.assertThat(instructionSerializer.sendSingleChunk(chunk2)).isEqualTo(LogcatInstructionSerializer.SEND_LOGCAT_RESULT_FAILURE_RETRIABLE); + Truth.assertThat(instructionSerializer.sendSingleChunk(chunk3)).isEqualTo(LogcatInstructionSerializer.SEND_LOGCAT_RESULT_FAILURE_RETRIABLE); + + Mockito.verifyNoInteractions(service); + } + + @Test(timeout = 3000L) + public void sendSingleChunk_always_returns_retriable_status_if_service_is_none_and_is_in_capture_mode() throws RemoteException { + instructionSerializer = new LogcatInstructionSerializer(PACKAGE_NAME); + + instructionSerializer.requestOneshotLogcat("brabra"); - SendLogcatRequest chunk1 = new SendLogcatRequest("tid1", new ArrayList<>(Arrays.asList("line1", "line2", "line3"))); - SendLogcatRequest chunk2 = new SendLogcatRequest("tid2", new ArrayList<>(Arrays.asList("line4", "line5", "line6"))); - SendLogcatRequest chunk3 = new SendLogcatRequest("tid3", new ArrayList<>(Arrays.asList("line7", "line8", "line9"))); + SendLogcatRequest chunk1 = new SendLogcatRequest("tid1", new ArrayList<>(Arrays.asList("line1", "line2", "line3")), "brabra"); + SendLogcatRequest chunk2 = new SendLogcatRequest("tid2", new ArrayList<>(Arrays.asList("line4", "line5", "line6")), "brabra"); + SendLogcatRequest chunk3 = new SendLogcatRequest("tid3", new ArrayList<>(Arrays.asList("line7", "line8", "line9")), "brabra"); doNothing().when(service).sendEvent(anyString(), anyString(), any(Bundle.class)); @@ -124,10 +141,10 @@ public void sendSingleChunk_uses_retry_barrier() throws RemoteException { Shadows.shadowOf(instructionSerializer.getHandler().getLooper()).pause(); - SendLogcatRequest noIssue = new SendLogcatRequest("noIssue", new ArrayList<>(Arrays.asList("line1", "line2", "line3"))); - SendLogcatRequest successAfterRetries = new SendLogcatRequest("successAfterRetries", new ArrayList<>(Arrays.asList("line4", "line5", "line6"))); - SendLogcatRequest retryExceeded = new SendLogcatRequest("retryExceeded", new ArrayList<>(Arrays.asList("line7", "line8", "line9"))); - SendLogcatRequest chunkRequest = new SendLogcatRequest("chunkRequest", new ArrayList<>(Arrays.asList("line10", "line11", "line12"))); + SendLogcatRequest noIssue = new SendLogcatRequest("noIssue", new ArrayList<>(Arrays.asList("line1", "line2", "line3")), null); + SendLogcatRequest successAfterRetries = new SendLogcatRequest("successAfterRetries", new ArrayList<>(Arrays.asList("line4", "line5", "line6")), null); + SendLogcatRequest retryExceeded = new SendLogcatRequest("retryExceeded", new ArrayList<>(Arrays.asList("line7", "line8", "line9")), null); + SendLogcatRequest chunkRequest = new SendLogcatRequest("chunkRequest", new ArrayList<>(Arrays.asList("line10", "line11", "line12")), null); SendLogcatRequest beginningRequest = SendLogcatRequest.createBeginning("beginningRequest"); SendLogcatRequest terminationRequest = SendLogcatRequest.createTermination("terminationRequest"); @@ -188,7 +205,15 @@ public void requestSendingLogcat_works_regardless_of_service() throws RemoteExce // don't fail for (int i = 0; i < 10; i++) { - instructionSerializer.requestOneshotLogcat(); + instructionSerializer.requestOneshotLogcat(null); + } + + Shadows.shadowOf(instructionSerializer.getHandler().getLooper()).idle(); + + // don't fail + + for (int i = 0; i < 10; i++) { + instructionSerializer.requestOneshotLogcat("brabra"); } Shadows.shadowOf(instructionSerializer.getHandler().getLooper()).idle(); @@ -203,10 +228,19 @@ public void requestSendingLogcat_does_nothing_if_disabled() throws RemoteExcepti instructionSerializer.setEnabled(false); for (int i = 0; i < 30; i++) { - if (i % 2 == 0) { - Truth.assertThat(instructionSerializer.requestOneshotLogcat()).isFalse(); - } else { - Truth.assertThat(instructionSerializer.requestStreamedLogcat("bsk")).isFalse(); + switch (i % 3) { + case 0: { + Truth.assertThat(instructionSerializer.requestOneshotLogcat(null)).isFalse(); + break; + } + case 1: { + Truth.assertThat(instructionSerializer.requestStreamedLogcat("bsk")).isFalse(); + break; + } + case 2: { + Truth.assertThat(instructionSerializer.requestOneshotLogcat("brabra")).isFalse(); + break; + } } } @@ -214,10 +248,19 @@ public void requestSendingLogcat_does_nothing_if_disabled() throws RemoteExcepti instructionSerializer.connect(service); for (int i = 0; i < 30; i++) { - if (i % 2 == 0) { - Truth.assertThat(instructionSerializer.requestOneshotLogcat()).isFalse(); - } else { - Truth.assertThat(instructionSerializer.requestStreamedLogcat("bsk")).isFalse(); + switch (i % 3) { + case 0: { + Truth.assertThat(instructionSerializer.requestOneshotLogcat(null)).isFalse(); + break; + } + case 1: { + Truth.assertThat(instructionSerializer.requestStreamedLogcat("bsk")).isFalse(); + break; + } + case 2: { + Truth.assertThat(instructionSerializer.requestOneshotLogcat("brabra")).isFalse(); + break; + } } } diff --git a/sdk/src/test/java/com/deploygate/sdk/LogcatProcessTest.java b/sdk/src/test/java/com/deploygate/sdk/LogcatProcessTest.java index 4a0dd0d..6b27d49 100644 --- a/sdk/src/test/java/com/deploygate/sdk/LogcatProcessTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/LogcatProcessTest.java @@ -1,5 +1,10 @@ package com.deploygate.sdk; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mockStatic; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.deploygate.sdk.helper.FakeLogcat; @@ -22,9 +27,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mockStatic; - @RunWith(AndroidJUnit4.class) public class LogcatProcessTest { private FakeLogcat fakeLogcat; @@ -72,7 +74,7 @@ public void nonOneShot_emits_multiple_log_chunks() { try { fakeLogcat = new FakeLogcat(10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk1", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk1", null, capture); watcher.run(); @@ -89,7 +91,7 @@ public void nonOneShot_emits_multiple_log_chunks() { try { fakeLogcat = new FakeLogcat(501); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks2", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk2", null, capture); watcher.run(); @@ -110,7 +112,7 @@ public void nonOneShot_emits_multiple_log_chunks() { try { fakeLogcat = new FakeLogcat(1000); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks3", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks3", null, capture); watcher.run(); @@ -136,7 +138,7 @@ public void OneShot_emits_single_log_chunk() { try { fakeLogcat = new FakeLogcat(10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, null, capture); watcher.run(); @@ -153,7 +155,7 @@ public void OneShot_emits_single_log_chunk() { try { fakeLogcat = new FakeLogcat(501); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, "cap2", capture); watcher.run(); @@ -173,7 +175,7 @@ public void OneShot_emits_single_log_chunk() { try { fakeLogcat = new FakeLogcat(1000); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, null, capture); watcher.run(); @@ -194,17 +196,17 @@ public void OneShot_emits_single_log_chunk() { @Test(timeout = 3000L) public void nonOneShot_interrupt_stops_later_emits() { - CaptureCallback capture = new CaptureCallback(); + CaptureCallback callback = new CaptureCallback(); try { // call the method for the finished process fakeLogcat = new FakeLogcat(10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks1", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks1", null, callback); watcher.run(); - List> linesList = capture.captured.get(watcher.getProcessId()); + List> linesList = callback.captured.get(watcher.getProcessId()); Truth.assertThat(linesList).hasSize(1); Truth.assertThat(linesList.get(0)).hasSize(10); @@ -221,13 +223,13 @@ public void nonOneShot_interrupt_stops_later_emits() { // interrupt the on-going process that read lines less than MAX_LINES fakeLogcat = new FakeLogcat(20, 10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks2", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk2", null, callback); destroyWorkerAfter(watcher, 300, TimeUnit.MILLISECONDS); watcher.run(); - List> linesList = capture.captured.get(watcher.getProcessId()); + List> linesList = callback.captured.get(watcher.getProcessId()); Truth.assertThat(linesList).isEmpty(); } finally { @@ -240,7 +242,7 @@ public void nonOneShot_interrupt_stops_later_emits() { // interrupt the on-going process that read many lines more than MAX_LINES fakeLogcat = new FakeLogcat(550, 549); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bks3", capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk3", null, callback); destroyWorkerAfter(watcher, 500, TimeUnit.MILLISECONDS); @@ -248,7 +250,7 @@ public void nonOneShot_interrupt_stops_later_emits() { List generatedLines = fakeLogcat.getGeneratedLines(); - List> linesList = capture.captured.get(watcher.getProcessId()); + List> linesList = callback.captured.get(watcher.getProcessId()); // emit the first chunk but second chunk Truth.assertThat(linesList).hasSize(1); @@ -269,7 +271,7 @@ public void OneShot_interrupt_stops_emitting_logs() { // call the method for the finished process fakeLogcat = new FakeLogcat(10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher("bsk1", null, capture); watcher.run(); @@ -290,7 +292,7 @@ public void OneShot_interrupt_stops_emitting_logs() { // interrupt the on-going process that read lines less than MAX_LINES fakeLogcat = new FakeLogcat(20, 10); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, "cap2", capture); destroyWorkerAfter(watcher, 300, TimeUnit.MILLISECONDS); @@ -309,7 +311,7 @@ public void OneShot_interrupt_stops_emitting_logs() { // interrupt the on-going process that read many lines more than MAX_LINES fakeLogcat = new FakeLogcat(550, 549); - LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, capture); + LogcatProcess.LogcatWatcher watcher = new LogcatProcess.LogcatWatcher(null, null, capture); destroyWorkerAfter(watcher, 300, TimeUnit.MILLISECONDS); @@ -350,7 +352,7 @@ private static class CaptureCallback implements LogcatProcess.Callback { private final Map finished = new HashMap<>(); @Override - public void onStarted(String processId) { + public void onStarted(@NonNull String processId) { if (captured.containsKey(processId)) { throw new IllegalStateException("only unique process id is allowed"); } @@ -360,14 +362,15 @@ public void onStarted(String processId) { @Override public void emit( - String processId, - ArrayList logcatLines + @NonNull String processId, + @NonNull ArrayList logcatLines, + @Nullable String captureId ) { captured.get(processId).add(logcatLines); } @Override - public void onFinished(String processId) { + public void onFinished(@NonNull String processId) { captured.put(processId, Collections.unmodifiableList(captured.get(processId))); finished.put(processId, true); } diff --git a/sdk/src/test/java/com/deploygate/sdk/SendLogcatRequestTest.java b/sdk/src/test/java/com/deploygate/sdk/SendLogcatRequestTest.java index 5bc5dc2..3231e0a 100644 --- a/sdk/src/test/java/com/deploygate/sdk/SendLogcatRequestTest.java +++ b/sdk/src/test/java/com/deploygate/sdk/SendLogcatRequestTest.java @@ -1,5 +1,7 @@ package com.deploygate.sdk; +import static com.google.common.truth.Truth.assertAbout; + import android.os.Bundle; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -19,8 +21,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; - -import static com.google.common.truth.Truth.assertAbout; +import java.util.Objects; @RunWith(AndroidJUnit4.class) public class SendLogcatRequestTest { @@ -37,21 +38,25 @@ public void apply() throws Throwable { SendLogcatRequest beginning = SendLogcatRequest.createBeginning("bsk1"); - BundleSubject.assertThat(beginning.toExtras()).isEqualTo(createLogExtra("bsk1", "unique_id", new ArrayList(), "beginning")); + BundleSubject.assertThat(beginning.toExtras()).isEqualTo(createLogExtra("bsk1", "unique_id", new ArrayList(), "beginning", null)); - SendLogcatRequest content = new SendLogcatRequest("bsk2", new ArrayList<>(Arrays.asList("1", "2"))); + SendLogcatRequest content = new SendLogcatRequest("bsk2", new ArrayList<>(Arrays.asList("1", "2")), null); - BundleSubject.assertThat(content.toExtras()).isEqualTo(createLogExtra("bsk2", "unique_id", new ArrayList<>(Arrays.asList("1", "2")), "content")); + BundleSubject.assertThat(content.toExtras()).isEqualTo(createLogExtra("bsk2", "unique_id", new ArrayList<>(Arrays.asList("1", "2")), "content", null)); SendLogcatRequest termination = SendLogcatRequest.createTermination("bsk3"); - BundleSubject.assertThat(termination.toExtras()).isEqualTo(createLogExtra("bsk3", "unique_id", new ArrayList(), "termination")); + BundleSubject.assertThat(termination.toExtras()).isEqualTo(createLogExtra("bsk3", "unique_id", new ArrayList(), "termination", null)); + + SendLogcatRequest capture = new SendLogcatRequest("bsk4", new ArrayList<>(Arrays.asList("1")), "capture_id"); + + BundleSubject.assertThat(capture.toExtras()).isEqualTo(createLogExtra("bsk4", "unique_id", new ArrayList<>(Arrays.asList("1")), "content", "capture_id")); } } @Test public void splitInto_create_n_sublist() { - SendLogcatRequest request = new SendLogcatRequest("bsk", arrayListOf(0, 10)); + SendLogcatRequest request = new SendLogcatRequest("bsk", arrayListOf(0, 10), null); List singleRequests = request.splitInto(1); @@ -61,39 +66,47 @@ public void splitInto_create_n_sublist() { List twoRequests = request.splitInto(2); Truth.assertThat(twoRequests).hasSize(2); - SendLogcatRequestSubject.assertThat(twoRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 5)); - SendLogcatRequestSubject.assertThat(twoRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 5)); + SendLogcatRequestSubject.assertThat(twoRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 5), null); + SendLogcatRequestSubject.assertThat(twoRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 5), null); List threeRequests = request.splitInto(3); Truth.assertThat(threeRequests).hasSize(3); - SendLogcatRequestSubject.assertThat(threeRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 3)); - SendLogcatRequestSubject.assertThat(threeRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 3)); - SendLogcatRequestSubject.assertThat(threeRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 4)); + SendLogcatRequestSubject.assertThat(threeRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 3), null); + SendLogcatRequestSubject.assertThat(threeRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 3), null); + SendLogcatRequestSubject.assertThat(threeRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 4), null); List nineRequests = request.splitInto(9); Truth.assertThat(nineRequests).hasSize(9); - SendLogcatRequestSubject.assertThat(nineRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(1, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(2, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(3)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(4)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(4, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(5)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(6)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(7)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(7, 1)); - SendLogcatRequestSubject.assertThat(nineRequests.get(8)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(8, 2)); + SendLogcatRequestSubject.assertThat(nineRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(1, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(2, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(3)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(4)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(4, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(5)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(6)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(7)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(7, 1), null); + SendLogcatRequestSubject.assertThat(nineRequests.get(8)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(8, 2), null); List overSplitRequests = request.splitInto(11); Truth.assertThat(overSplitRequests).hasSize(10); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(1, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(2, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(3)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(4)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(4, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(5)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(6)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(7)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(7, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(8)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(8, 1)); - SendLogcatRequestSubject.assertThat(overSplitRequests.get(9)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(9, 1)); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(0)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(1)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(1, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(2)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(2, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(3)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(4)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(4, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(5)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(5, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(6)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(7)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(7, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(8)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(8, 1), null); + SendLogcatRequestSubject.assertThat(overSplitRequests.get(9)).isSameInBundle(request.gid, SendLogcatRequest.Position.Content, arrayListOf(9, 1), null); + + SendLogcatRequest requestWithCapture = new SendLogcatRequest("bsk2", arrayListOf(0, 10), "capture_id"); + + List threeRequestsWithCapture = requestWithCapture.splitInto(3); + Truth.assertThat(threeRequestsWithCapture).hasSize(3); + SendLogcatRequestSubject.assertThat(threeRequestsWithCapture.get(0)).isSameInBundle(requestWithCapture.gid, SendLogcatRequest.Position.Content, arrayListOf(0, 3), "capture_id"); + SendLogcatRequestSubject.assertThat(threeRequestsWithCapture.get(1)).isSameInBundle(requestWithCapture.gid, SendLogcatRequest.Position.Content, arrayListOf(3, 3), "capture_id"); + SendLogcatRequestSubject.assertThat(threeRequestsWithCapture.get(2)).isSameInBundle(requestWithCapture.gid, SendLogcatRequest.Position.Content, arrayListOf(6, 4), "capture_id"); } @Test @@ -130,17 +143,24 @@ private static Bundle createLogExtra( String gid, String cid, ArrayList lines, - String positionLabel + String positionLabel, + String captureId ) { Bundle bundle = new Bundle(); bundle.putString("e.gid", gid); bundle.putString("e.cid", cid); bundle.putStringArrayList("log", lines); bundle.putString("e.bundle-position", positionLabel); + if (captureId != null) { + bundle.putString("e.capture-id", captureId); + } return bundle; } - private static ArrayList arrayListOf(int startInclusive, int count) { + private static ArrayList arrayListOf( + int startInclusive, + int count + ) { ArrayList list = new ArrayList<>(); for (int i = startInclusive, max = startInclusive + count; i < max; i++) { @@ -177,7 +197,12 @@ protected SendLogcatRequestSubject( this.actual = actual; } - public void isSameInBundle(String expectedKey, SendLogcatRequest.Position expectedPosition, List expectedLines) { + public void isSameInBundle( + String expectedKey, + SendLogcatRequest.Position expectedPosition, + List expectedLines, + String captureId + ) { if (!actual.gid.equals(expectedKey)) { failWithActual(Fact.simpleFact(String.format(Locale.US, "%s is expected of %s", expectedKey, toString(actual)))); return; @@ -199,6 +224,10 @@ public void isSameInBundle(String expectedKey, SendLogcatRequest.Position expect return; } } + + if (!Objects.equals(actual.captureId, captureId)) { + failWithActual(Fact.simpleFact(String.format(Locale.US, "%s is expected to be exactly included in %s", actual.captureId, captureId))); + } } @Override @@ -225,6 +254,8 @@ private static String toString(SendLogcatRequest request) { builder.append(request.position.name()); builder.append(", lines=["); builder.append(String.join(", ", request.lines)); + builder.append(", capture_id="); + builder.append(request.captureId); builder.append("]"); builder.append(" }"); return builder.toString(); diff --git a/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java b/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java index a9f8771..471df33 100644 --- a/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java +++ b/sdk/src/test/java/com/deploygate/sdk/mockito/BundleMatcher.java @@ -1,13 +1,13 @@ package com.deploygate.sdk.mockito; +import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress; + 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)); diff --git a/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java b/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java index 0e8402b..719483f 100644 --- a/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java +++ b/sdk/src/test/java/com/deploygate/sdk/truth/BundleSubject.java @@ -1,5 +1,7 @@ package com.deploygate.sdk.truth; +import static com.google.common.truth.Truth.assertAbout; + import android.os.Bundle; import com.deploygate.sdk.helper.Bundles; @@ -9,8 +11,6 @@ import java.util.Locale; -import static com.google.common.truth.Truth.assertAbout; - public class BundleSubject extends Subject { public static Factory bundles() { return new Factory() {