diff --git a/README.md b/README.md index 237a6a2d0..3d210d11a 100644 --- a/README.md +++ b/README.md @@ -78,32 +78,52 @@ The main Chucker activity is launched in its own task, allowing it to be display ## Configure 🎨 -You can customize chucker providing an instance of a `ChuckerCollector`: +You can customize Chucker by calling the DSL `configureChucker`. +It may be used to disable one of the features (HTTP or Errors). ```kotlin -// Create the Collector -val chuckerCollector = ChuckerCollector( - context = this, - // Toggles visibility of the push notification - showNotification = true, - // Allows to customize the retention period of collected data +configureChucker { + http { + enabled = true + showNotification = true retentionPeriod = RetentionManager.Period.ONE_HOUR -) - -// Create the Interceptor -val chuckerInterceptor = ChuckerInterceptor( - context = this, - // The previously created Collector - collector = chuckerCollector, - // The max body content length, after this responses will be truncated. - maxContentLength = 250000L, - // List of headers to obfuscate in the Chucker UI - headersToRedact = listOf("Auth-Token")) - -// Don't forget to plug the ChuckerInterceptor inside the OkHttpClient -val client = OkHttpClient.Builder() - .addInterceptor(chuckerInterceptor) - .build() + maxContentLength = 250000L + headers { + redact("Authorization") + redact("Auth-Token") + redact("User-Session") + } + } + error { + enabled = true + showNotification = true + } +} +``` + +### Configure for Java + +```java +HashSet headersToRedact = new HashSet<>(); +headersToRedact.add("Authorization"); +headersToRedact.add("Auth-Token"); +headersToRedact.add("User-Session"); + +List features = Arrays.asList( + new HttpFeature( + true, + true, + RetentionManager.Period.ONE_HOUR, + DEFAULT_MAX_CONTENT_LENGTH, + headersToRedact + ), + new ErrorsFeature( + true, + true + ) +); + +ChuckerJavaConfig.configure(features); ``` ### Throwables ☄️ @@ -113,7 +133,7 @@ Chucker supports also collecting and displaying **Throwables** of your applicati ```kotlin try { // Do something risky -} catch (IOException exception) { +} catch (exception: IOException) { chuckerCollector.onError("TAG", exception) } ``` @@ -127,8 +147,8 @@ It is intended for **use during development**, and not in release builds or othe You can redact headers that contain sensitive information by calling `redactHeader(String)` on the `ChuckerInterceptor`. ```kotlin -interceptor.redactHeader("Auth-Token"); -interceptor.redactHeader("User-Session"); +interceptor.redactHeader("Auth-Token") +interceptor.redactHeader("User-Session") ``` ## Migrating 🚗 diff --git a/build.gradle b/build.gradle index 43c6415bf..6833a4966 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ buildscript { retrofitVersion = '2.5.0' roomVersion = '2.1.0' supportLibVersion = '1.1.0-rc01' + fragmentVersion = '1.1.0' } repositories { diff --git a/library-no-op/build.gradle b/library-no-op/build.gradle index 9c27ef164..e84c5159a 100644 --- a/library-no-op/build.gradle +++ b/library-no-op/build.gradle @@ -21,6 +21,7 @@ artifacts { dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttp3Version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "androidx.fragment:fragment:$fragmentVersion" } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt index 19b4c80bb..5fbaa1f39 100644 --- a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt @@ -5,11 +5,15 @@ import android.content.Context /** * No-op implementation. */ -class ChuckerCollector @JvmOverloads constructor( - context: Context, - var showNotification: Boolean = true, - var retentionPeriod: RetentionManager.Period = RetentionManager.Period.ONE_WEEK +class ChuckerCollector( + context: Context ) { + @Deprecated("This constructor will disappear in a following version.") + constructor( + context: Context, + showNotification: Boolean, + retentionPeriod: RetentionManager.Period + ) : this(context) fun onError(obj: Any?, obj2: Any?) { // Empty method for the library-no-op artifact diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index 537691a64..905fada57 100644 --- a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -10,11 +10,17 @@ import okhttp3.Response */ class ChuckerInterceptor @JvmOverloads constructor( context: Context, - collector: Any? = null, - maxContentLength: Any? = null, - headersToRedact: Any? = null + collector: Any? = null ) : Interceptor { + @Deprecated("This constructor will disappear in a following version.") + constructor( + context: Context, + collector: Any? = null, + maxContentLength: Any? = null, + headersToRedact: Any? = null + ) : this(context, collector) + fun redactHeaders(vararg names: String): ChuckerInterceptor { return this } diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt index c105fde10..74bdd5310 100644 --- a/library-no-op/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt @@ -5,9 +5,8 @@ import android.content.Context /** * No-op implementation. */ -class RetentionManager @JvmOverloads constructor( - context: Context, - retentionPeriod: Any? = null +class RetentionManager( + context: Context ) { @Synchronized diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt new file mode 100644 index 000000000..e4ef28ca4 --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt @@ -0,0 +1,7 @@ +@file:JvmName("ChuckerJavaConfig") + +package com.chuckerteam.chucker.api.config + +fun configure(features: List) { + // Empty method for the library-no-op artifact +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt new file mode 100644 index 000000000..46e67fc6e --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt @@ -0,0 +1,19 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import com.chuckerteam.chucker.api.internal.EmptyFragment + +class ErrorsFeature( + override var enabled: Boolean, + var showNotification: Boolean +) : TabFeature { + override val name: Int = 0 + + override val id: Int = 0 + + override fun newFragment() = EmptyFragment() + + override fun dismissNotification(context: Context) { + // Empty method for the library-no-op artifact + } +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt new file mode 100644 index 000000000..b7ed57791 --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt @@ -0,0 +1,23 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import com.chuckerteam.chucker.api.RetentionManager +import com.chuckerteam.chucker.api.internal.EmptyFragment + +class HttpFeature( + override var enabled: Boolean, + var showNotification: Boolean, + var retentionPeriod: RetentionManager.Period, + var maxContentLength: Long, + var headersToRedact: MutableSet +) : TabFeature { + override val name: Int = 0 + + override val id: Int = 0 + + override fun newFragment() = EmptyFragment() + + override fun dismissNotification(context: Context) { + // Empty method for the library-no-op artifact + } +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt new file mode 100644 index 000000000..9e9f6af9c --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt @@ -0,0 +1,12 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import androidx.fragment.app.Fragment + +interface TabFeature { + val name: Int + val id: Int + var enabled: Boolean + fun newFragment(): Fragment + fun dismissNotification(context: Context) +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt new file mode 100644 index 000000000..38aa33e00 --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt @@ -0,0 +1,47 @@ +package com.chuckerteam.chucker.api.dsl + +import com.chuckerteam.chucker.api.RetentionManager + +const val DEFAULT_MAX_CONTENT_LENGTH = 250000L + +@DslMarker +annotation class ChuckerConfig + +@ChuckerConfig +fun configureChucker(config: ChuckerConfigBuilder.() -> Unit) = Unit + +@ChuckerConfig +class ChuckerConfigBuilder { + + @ChuckerConfig + fun http(block: HttpFeatureBuilder.() -> Unit) = Unit + + @ChuckerConfig + fun error(block: ErrorsFeatureBuilder.() -> Unit) = Unit + + fun build() = Unit +} + +@ChuckerConfig +class HttpFeatureBuilder { + var enabled: Boolean = true + var showNotification: Boolean = true + var retentionPeriod: RetentionManager.Period = RetentionManager.Period.ONE_WEEK + var maxContentLength: Long = 0 + internal var headersToRedact: MutableSet = mutableSetOf() + + @ChuckerConfig + fun headers(redactHeaders: RedactHeaders.() -> Unit) = Unit +} + +@ChuckerConfig +class RedactHeaders { + @ChuckerConfig + fun redact(header: String) = Unit +} + +@ChuckerConfig +class ErrorsFeatureBuilder { + var enabled: Boolean = true + var showNotification: Boolean = true +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/internal/EmptyFragment.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/internal/EmptyFragment.kt new file mode 100644 index 000000000..78f1eeb87 --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/internal/EmptyFragment.kt @@ -0,0 +1,5 @@ +package com.chuckerteam.chucker.api.internal + +import androidx.fragment.app.Fragment + +class EmptyFragment : Fragment() diff --git a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt index 293802010..4892a7b81 100644 --- a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerCollector.kt @@ -1,9 +1,12 @@ package com.chuckerteam.chucker.api import android.content.Context +import com.chuckerteam.chucker.api.config.ErrorsFeature +import com.chuckerteam.chucker.api.config.HttpFeature import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.data.entity.RecordedThrowable import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider +import com.chuckerteam.chucker.internal.support.FeatureManager import com.chuckerteam.chucker.internal.support.NotificationHelper /** @@ -12,19 +15,29 @@ import com.chuckerteam.chucker.internal.support.NotificationHelper * provide it to * * @param context An Android Context - * @param showNotification Control whether a notification is shown while HTTP activity - * is recorded. - * @param retentionManager Set the retention period for HTTP transaction data captured - * by this collector. The default is one week. */ -class ChuckerCollector @JvmOverloads constructor( - context: Context, - var showNotification: Boolean = true, - retentionPeriod: RetentionManager.Period = RetentionManager.Period.ONE_WEEK +class ChuckerCollector( + context: Context ) { - private val retentionManager: RetentionManager = RetentionManager(context, retentionPeriod) + @Deprecated("This constructor will disappear in a following version.") + constructor( + context: Context, + showNotification: Boolean, + retentionPeriod: RetentionManager.Period + ) : this(context) { + // This 3 lines are here to avoid breaking changes in the constructor signature + // They will disappear when we will make the breaking changes. + httpFeature.showNotification = showNotification + httpFeature.retentionPeriod = retentionPeriod + errorsFeature.showNotification = showNotification + } + + private val retentionManager: RetentionManager = RetentionManager(context) private val notificationHelper: NotificationHelper = NotificationHelper(context) + private val httpFeature: HttpFeature = FeatureManager.find() + private val errorsFeature: ErrorsFeature = FeatureManager.find() + init { RepositoryProvider.initialize(context) } @@ -35,9 +48,11 @@ class ChuckerCollector @JvmOverloads constructor( * @param throwable The triggered [Throwable] */ fun onError(tag: String, throwable: Throwable) { + if (!errorsFeature.enabled) return + val recordedThrowable = RecordedThrowable(tag, throwable) RepositoryProvider.throwable().saveThrowable(recordedThrowable) - if (showNotification) { + if (errorsFeature.showNotification) { notificationHelper.show(recordedThrowable) } retentionManager.doMaintenance() @@ -48,8 +63,10 @@ class ChuckerCollector @JvmOverloads constructor( * @param transaction The HTTP transaction sent */ internal fun onRequestSent(transaction: HttpTransaction) { + if (!httpFeature.enabled) return + RepositoryProvider.transaction().insertTransaction(transaction) - if (showNotification) { + if (httpFeature.showNotification) { notificationHelper.show(transaction) } retentionManager.doMaintenance() @@ -61,8 +78,10 @@ class ChuckerCollector @JvmOverloads constructor( * @param transaction The sent HTTP transaction completed with the response */ internal fun onResponseReceived(transaction: HttpTransaction) { + if (!httpFeature.enabled) return + val updated = RepositoryProvider.transaction().updateTransaction(transaction) - if (showNotification && updated > 0) { + if (httpFeature.showNotification && updated > 0) { notificationHelper.show(transaction) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index 6e151d5ff..548df8332 100755 --- a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -3,7 +3,10 @@ package com.chuckerteam.chucker.api import android.content.Context import android.util.Log import com.chuckerteam.chucker.api.Chucker.LOG_TAG +import com.chuckerteam.chucker.api.config.HttpFeature +import com.chuckerteam.chucker.api.dsl.DEFAULT_MAX_CONTENT_LENGTH import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.chuckerteam.chucker.internal.support.FeatureManager import com.chuckerteam.chucker.internal.support.IOUtils import com.chuckerteam.chucker.internal.support.hasBody import java.io.IOException @@ -12,6 +15,7 @@ import java.nio.charset.UnsupportedCharsetException import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.Interceptor +import okhttp3.Request import okhttp3.Response import okio.Buffer import okio.BufferedSource @@ -24,61 +28,45 @@ private const val MAX_BLOB_SIZE = 1000_000L * * @param context An Android [Context] * @param collector A [ChuckerCollector] to customize data retention - * @param maxContentLength The maximum length for request and response content - * before they are truncated. Warning: setting this value too high may cause unexpected - * results. - * @param headersToRedact List of headers that you want to redact. They will be not be shown in - * the ChuckerUI but will be replaced with a `**`. */ class ChuckerInterceptor @JvmOverloads constructor( private val context: Context, - private val collector: ChuckerCollector = ChuckerCollector(context), - private val maxContentLength: Long = 250000L, - private val headersToRedact: MutableSet = mutableSetOf() + private val collector: ChuckerCollector = ChuckerCollector(context) ) : Interceptor { + @Deprecated("This constructor will disappear in a following version.") + constructor( + context: Context, + collector: ChuckerCollector = ChuckerCollector(context), + maxContentLength: Long = DEFAULT_MAX_CONTENT_LENGTH, + headersToRedact: MutableSet = mutableSetOf() + ) : this(context, collector) { + // This 2 lines are here to avoid breaking changes in the constructor signature + // They will disappear when we will make the breaking changes. + httpFeature.maxContentLength = maxContentLength + httpFeature.headersToRedact = headersToRedact + } + private val io: IOUtils = IOUtils(context) + private val httpFeature: HttpFeature = FeatureManager.find() fun redactHeader(name: String) = apply { - headersToRedact.add(name) + httpFeature.headersToRedact.add(name) } @Throws(IOException::class) - @Suppress("LongMethod", "ComplexMethod") override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() - val requestBody = request.body() - val transaction = HttpTransaction() - transaction.apply { - requestDate = System.currentTimeMillis() - method = request.method() - populateUrl(request.url().toString()) - setRequestHeaders(request.headers()) - requestContentType = requestBody?.contentType()?.toString() - requestContentLength = requestBody?.contentLength() ?: 0L - } - - val encodingIsSupported = io.bodyHasSupportedEncoding(request.headers().get("Content-Encoding")) - transaction.isRequestBodyPlainText = encodingIsSupported - - if (requestBody != null && encodingIsSupported) { - val source = io.getNativeSource(Buffer(), io.bodyIsGzipped(request.headers().get("Content-Encoding"))) - val buffer = source.buffer() - requestBody.writeTo(buffer) - var charset: Charset = UTF8 - val contentType = requestBody.contentType() - if (contentType != null) { - charset = contentType.charset(UTF8) ?: UTF8 - } - if (io.isPlaintext(buffer)) { - val content = io.readFromBuffer(buffer, charset, maxContentLength) - transaction.requestBody = content - } else { - transaction.isResponseBodyPlainText = false - } + return if (httpFeature.enabled) { + captureTransaction(request, chain) + } else { + chain.proceed(request) } + } + private fun captureTransaction(request: Request, chain: Interceptor.Chain): Response { + val transaction = captureRequest(request) collector.onRequestSent(transaction) val startNs = System.nanoTime() @@ -126,17 +114,7 @@ class ChuckerInterceptor @JvmOverloads constructor( return response } } - if (io.isPlaintext(buffer)) { - val content = io.readFromBuffer(buffer.clone(), charset, maxContentLength) - transaction.responseBody = content - } else { - transaction.isResponseBodyPlainText = false - - if (transaction.responseContentType?.contains("image") == true && buffer.size() < MAX_BLOB_SIZE) { - transaction.responseImageData = buffer.clone().readByteArray() - } - } - transaction.responseContentLength = buffer.size() + transaction.completeWithBody(buffer, charset) } collector.onResponseReceived(transaction) @@ -144,11 +122,46 @@ class ChuckerInterceptor @JvmOverloads constructor( return response } - /** Overrides all the headers in [headersToRedact] with a `**` */ + private fun captureRequest(request: Request): HttpTransaction { + val requestBody = request.body() + val transaction = HttpTransaction() + + transaction.apply { + requestDate = System.currentTimeMillis() + method = request.method() + populateUrl(request.url().toString()) + setRequestHeaders(request.headers()) + requestContentType = requestBody?.contentType()?.toString() + requestContentLength = requestBody?.contentLength() ?: 0L + } + + val encodingIsSupported = io.bodyHasSupportedEncoding(request.headers().get("Content-Encoding")) + transaction.isRequestBodyPlainText = encodingIsSupported + + if (requestBody != null && encodingIsSupported) { + val source = io.getNativeSource(Buffer(), io.bodyIsGzipped(request.headers().get("Content-Encoding"))) + val buffer = source.buffer() + requestBody.writeTo(buffer) + var charset: Charset = UTF8 + val contentType = requestBody.contentType() + if (contentType != null) { + charset = contentType.charset(UTF8) ?: UTF8 + } + if (io.isPlaintext(buffer)) { + val content = io.readFromBuffer(buffer, charset, httpFeature.maxContentLength) + transaction.requestBody = content + } else { + transaction.isResponseBodyPlainText = false + } + } + return transaction + } + + /** Overrides all the headers in [HttpFeature.headersToRedact] with a `**` */ private fun filterHeaders(headers: Headers): Headers { val builder = headers.newBuilder() for (name in headers.names()) { - if (name in headersToRedact) { + if (name in httpFeature.headersToRedact) { builder.set(name, "**") } } @@ -161,8 +174,8 @@ class ChuckerInterceptor @JvmOverloads constructor( @Throws(IOException::class) private fun getNativeSource(response: Response): BufferedSource { if (io.bodyIsGzipped(response.headers().get("Content-Encoding"))) { - val source = response.peekBody(maxContentLength).source() - if (source.buffer().size() < maxContentLength) { + val source = response.peekBody(httpFeature.maxContentLength).source() + if (source.buffer().size() < httpFeature.maxContentLength) { return io.getNativeSource(source, true) } else { Log.w(LOG_TAG, "gzip encoded response was too long") @@ -171,6 +184,20 @@ class ChuckerInterceptor @JvmOverloads constructor( return response.body()!!.source() } + private fun HttpTransaction.completeWithBody(buffer: Buffer, charset: Charset) { + if (io.isPlaintext(buffer)) { + val content = io.readFromBuffer(buffer.clone(), charset, httpFeature.maxContentLength) + this.responseBody = content + } else { + this.isResponseBodyPlainText = false + + if (this.responseContentType?.contains("image") == true && buffer.size() < MAX_BLOB_SIZE) { + this.responseImageData = buffer.clone().readByteArray() + } + } + this.responseContentLength = buffer.size() + } + companion object { private val UTF8 = Charset.forName("UTF-8") } diff --git a/library/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt b/library/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt index e68dfbbc5..e1d93e31f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/RetentionManager.kt @@ -4,29 +4,31 @@ import android.content.Context import android.content.SharedPreferences import android.util.Log import com.chuckerteam.chucker.api.Chucker.LOG_TAG +import com.chuckerteam.chucker.api.config.HttpFeature import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider +import com.chuckerteam.chucker.internal.support.FeatureManager import java.util.concurrent.TimeUnit /** * Class responsible of holding the logic for the retention of your HTTP transactions * and your throwable. You can customize how long data should be stored here. * @param context An Android Context - * @param retentionPeriod A [Period] to specify the retention of data. Default 1 week. */ @Suppress("MagicNumber") -class RetentionManager @JvmOverloads constructor( - context: Context, - retentionPeriod: Period = Period.ONE_WEEK +class RetentionManager( + context: Context ) { + private val httpFeature: HttpFeature = FeatureManager.find() + // The actual retention period in milliseconds (default to ONE_WEEK) - private val period: Long = toMillis(retentionPeriod) + private val period: Long = toMillis(httpFeature.retentionPeriod) // How often the cleanup should happen private val cleanupFrequency: Long private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0) init { - cleanupFrequency = if (retentionPeriod == Period.ONE_HOUR) + cleanupFrequency = if (httpFeature.retentionPeriod == Period.ONE_HOUR) TimeUnit.MINUTES.toMillis(30) else TimeUnit.HOURS.toMillis(2) diff --git a/library/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt b/library/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt new file mode 100644 index 000000000..f72b30377 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/config/ChuckerJavaConfig.kt @@ -0,0 +1,9 @@ +@file:JvmName("ChuckerJavaConfig") + +package com.chuckerteam.chucker.api.config + +import com.chuckerteam.chucker.internal.support.FeatureManager + +fun configure(features: List) { + features.forEach(FeatureManager::configure) +} diff --git a/library/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt b/library/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt new file mode 100644 index 000000000..7132aa95b --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/config/ErrorsFeature.kt @@ -0,0 +1,32 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import androidx.fragment.app.Fragment +import com.chuckerteam.chucker.R +import com.chuckerteam.chucker.api.Chucker +import com.chuckerteam.chucker.internal.ui.error.ErrorListFragment + +class ErrorsFeature( + override var enabled: Boolean, + var showNotification: Boolean +) : TabFeature { + override val name: Int = R.string.chucker_tab_errors + + override val id: Int = Chucker.SCREEN_ERROR + + override fun newFragment(): Fragment { + return ErrorListFragment.newInstance() + } + + override fun dismissNotification(context: Context) { + Chucker.dismissErrorsNotification(context) + } + + companion object { + fun default(): ErrorsFeature = + ErrorsFeature( + enabled = true, + showNotification = true + ) + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt b/library/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt new file mode 100644 index 000000000..59cd812e4 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/config/HttpFeature.kt @@ -0,0 +1,40 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import androidx.fragment.app.Fragment +import com.chuckerteam.chucker.R +import com.chuckerteam.chucker.api.Chucker +import com.chuckerteam.chucker.api.RetentionManager +import com.chuckerteam.chucker.api.dsl.DEFAULT_MAX_CONTENT_LENGTH +import com.chuckerteam.chucker.internal.ui.transaction.TransactionListFragment + +class HttpFeature( + override var enabled: Boolean, + var showNotification: Boolean, + var retentionPeriod: RetentionManager.Period, + var maxContentLength: Long, + var headersToRedact: MutableSet +) : TabFeature { + override val name: Int = R.string.chucker_tab_network + + override val id: Int = Chucker.SCREEN_HTTP + + override fun newFragment(): Fragment { + return TransactionListFragment.newInstance() + } + + override fun dismissNotification(context: Context) { + Chucker.dismissTransactionsNotification(context) + } + + companion object { + fun default(): HttpFeature = + HttpFeature( + enabled = true, + showNotification = true, + retentionPeriod = RetentionManager.Period.ONE_WEEK, + headersToRedact = mutableSetOf(), + maxContentLength = DEFAULT_MAX_CONTENT_LENGTH + ) + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt b/library/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt new file mode 100644 index 000000000..57f9025f7 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/config/TabFeature.kt @@ -0,0 +1,14 @@ +package com.chuckerteam.chucker.api.config + +import android.content.Context +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment + +interface TabFeature { + @get:StringRes + val name: Int + val id: Int + var enabled: Boolean + fun newFragment(): Fragment + fun dismissNotification(context: Context) +} diff --git a/library/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt b/library/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt new file mode 100644 index 000000000..3524eb6af --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/dsl/Configuration.dsl.kt @@ -0,0 +1,91 @@ +package com.chuckerteam.chucker.api.dsl + +import com.chuckerteam.chucker.api.RetentionManager +import com.chuckerteam.chucker.api.config.ErrorsFeature +import com.chuckerteam.chucker.api.config.HttpFeature +import com.chuckerteam.chucker.internal.support.FeatureManager + +const val DEFAULT_MAX_CONTENT_LENGTH = 250000L + +@DslMarker +annotation class ChuckerConfig + +@ChuckerConfig +fun configureChucker(config: ChuckerConfigBuilder.() -> Unit) { + ChuckerConfigBuilder().apply(config).build() +} + +@ChuckerConfig +class ChuckerConfigBuilder { + + private var http: HttpFeature = HttpFeatureBuilder().build() + private var errors: ErrorsFeature = ErrorsFeatureBuilder().build() + + @ChuckerConfig + fun http(block: HttpFeatureBuilder.() -> Unit) { + http = HttpFeatureBuilder().apply(block).build() + } + + @ChuckerConfig + fun error(block: ErrorsFeatureBuilder.() -> Unit) { + errors = ErrorsFeatureBuilder().apply(block).build() + } + + fun build() { + FeatureManager.configure(http) + FeatureManager.configure(errors) + } +} + +@ChuckerConfig +class HttpFeatureBuilder { + var enabled: Boolean = true + /** + * Control whether a notification is shown while HTTP activity is recorded. + * The default is true. + */ + var showNotification: Boolean = true + + /** + * Set the retention period for HTTP transaction data captured by this collector. + * The default is one week. + */ + var retentionPeriod: RetentionManager.Period = RetentionManager.Period.ONE_WEEK + + /** + * The maximum length for request and response content before they are truncated. + * Warning: setting this value too high may cause unexpected results. + */ + var maxContentLength: Long = DEFAULT_MAX_CONTENT_LENGTH + + /** + * List of headers that you want to redact. They will be not be shown in + * the ChuckerUI but will be replaced with a `**`. + */ + internal var headersToRedact: MutableSet = mutableSetOf() + + fun build(): HttpFeature = + HttpFeature(enabled, showNotification, retentionPeriod, maxContentLength, headersToRedact) + + @ChuckerConfig + fun headers(redactHeaders: RedactHeaders.() -> Unit) { + RedactHeaders(this).apply(redactHeaders) + } +} + +@ChuckerConfig +class RedactHeaders(private val httpFeature: HttpFeatureBuilder) { + @ChuckerConfig + fun redact(header: String) { + httpFeature.headersToRedact.add(header) + } +} + +@ChuckerConfig +class ErrorsFeatureBuilder { + var enabled: Boolean = true + var showNotification: Boolean = true + + fun build(): ErrorsFeature = + ErrorsFeature(enabled, showNotification) +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FeatureManager.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FeatureManager.kt new file mode 100644 index 000000000..7e858beeb --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FeatureManager.kt @@ -0,0 +1,34 @@ +package com.chuckerteam.chucker.internal.support + +import com.chuckerteam.chucker.api.config.ErrorsFeature +import com.chuckerteam.chucker.api.config.HttpFeature +import com.chuckerteam.chucker.api.config.TabFeature + +internal object FeatureManager { + + private val features: MutableList = mutableListOf( + HttpFeature.default(), + ErrorsFeature.default() + ) + + fun configure(tabFeature: TabFeature) { + features.removeAll { it.javaClass == tabFeature.javaClass } + features.add(tabFeature) + } + + inline fun find(): T { + return features.firstOrNull { it is T } as T + } + + fun countEnabledFeatures(): Int { + return features.count { it.enabled } + } + + fun getAt(position: Int): TabFeature { + return features.filter { it.enabled }[position] + } + + fun getPositionOf(screenToShow: Int): Int { + return features.filter { it.enabled }.indexOfFirst { it.id == screenToShow } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/HomePageAdapter.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/HomePageAdapter.kt index e03e1997d..41bc0a6e7 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/HomePageAdapter.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/HomePageAdapter.kt @@ -4,34 +4,19 @@ import android.content.Context import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter -import com.chuckerteam.chucker.R -import com.chuckerteam.chucker.internal.ui.error.ErrorListFragment -import com.chuckerteam.chucker.internal.ui.transaction.TransactionListFragment +import com.chuckerteam.chucker.internal.support.FeatureManager import java.lang.ref.WeakReference internal class HomePageAdapter(context: Context, fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private val context: WeakReference = WeakReference(context) - override fun getItem(position: Int): Fragment = if (position == SCREEN_HTTP_INDEX) { - TransactionListFragment.newInstance() - } else { - ErrorListFragment.newInstance() - } + override fun getItem(position: Int): Fragment = + FeatureManager.getAt(position).newFragment() - override fun getCount(): Int = 2 + override fun getCount(): Int = FeatureManager.countEnabledFeatures() override fun getPageTitle(position: Int): CharSequence? = - context.get()?.getString( - if (position == SCREEN_HTTP_INDEX) { - R.string.chucker_tab_network - } else { - R.string.chucker_tab_errors - } - ) - - companion object { - const val SCREEN_HTTP_INDEX = 0 - const val SCREEN_ERROR_INDEX = 1 - } + context.get()?.getString(FeatureManager.getAt(position).name) } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainActivity.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainActivity.kt index 84e71578f..9d274ecc6 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainActivity.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.appcompat.widget.Toolbar import androidx.viewpager.widget.ViewPager import com.chuckerteam.chucker.R import com.chuckerteam.chucker.api.Chucker +import com.chuckerteam.chucker.internal.support.FeatureManager import com.chuckerteam.chucker.internal.ui.error.ErrorActivity import com.chuckerteam.chucker.internal.ui.error.ErrorAdapter import com.chuckerteam.chucker.internal.ui.transaction.TransactionActivity @@ -53,11 +54,7 @@ class MainActivity : viewPager.addOnPageChangeListener(object : TabLayout.TabLayoutOnPageChangeListener(tabLayout) { override fun onPageSelected(position: Int) { super.onPageSelected(position) - if (position == 0) { - Chucker.dismissTransactionsNotification(this@MainActivity) - } else { - Chucker.dismissErrorsNotification(this@MainActivity) - } + FeatureManager.getAt(position).dismissNotification(this@MainActivity) } }) consumeIntent(intent) @@ -74,11 +71,7 @@ class MainActivity : private fun consumeIntent(intent: Intent) { // Get the screen to show, by default => HTTP val screenToShow = intent.getIntExtra(EXTRA_SCREEN, Chucker.SCREEN_HTTP) - if (screenToShow == Chucker.SCREEN_HTTP) { - viewPager.currentItem = HomePageAdapter.SCREEN_HTTP_INDEX - } else { - viewPager.currentItem = HomePageAdapter.SCREEN_ERROR_INDEX - } + viewPager.currentItem = FeatureManager.getPositionOf(screenToShow) } override fun onErrorClick(throwableId: Long, position: Int) { diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ba96ef368..6684d2994 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ headersToRedact = new HashSet<>(); + headersToRedact.add("Authorization"); + headersToRedact.add("Auth-Token"); + headersToRedact.add("User-Session"); + + List features = Arrays.asList( + new HttpFeature( + true, + true, + RetentionManager.Period.ONE_HOUR, + DEFAULT_MAX_CONTENT_LENGTH, + headersToRedact + ), + new ErrorsFeature( + true, + true + ) + ); + + ChuckerJavaConfig.configure(features); + } +} diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt index f4f8583f0..4ff04a904 100644 --- a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt @@ -4,7 +4,6 @@ import android.content.Context import com.chuckerteam.chucker.api.Chucker import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.chuckerteam.chucker.api.RetentionManager import com.chuckerteam.chucker.sample.HttpBinApi.Data import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -21,15 +20,12 @@ class HttpBinClient( ) { private val collector = ChuckerCollector( - context = context, - showNotification = true, - retentionPeriod = RetentionManager.Period.ONE_HOUR + context = context ) private val chuckerInterceptor = ChuckerInterceptor( context = context, - collector = collector, - maxContentLength = 250000L + collector = collector ) private val httpClient =