├── .github ├── CODEOWNERS ├── FUNDING.yml ├── workflows │ ├── gradle-wrapper-validation.yml │ ├── close-and-release-repository.yaml │ ├── publish-release.yaml │ ├── publish-snapshot.yaml │ └── pre-merge.yaml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE ├── settings.gradle ├── .gitattributes ├── library ├── proguard-rules.pro └── src │ ├── test │ ├── resources │ │ └── sample_image.png │ └── kotlin │ │ └── com │ │ └── chuckerteam │ │ └── chucker │ │ ├── internal │ │ ├── data │ │ │ ├── har │ │ │ │ ├── PostDataTest.kt │ │ │ │ ├── ContentTest.kt │ │ │ │ ├── QueryStringTest.kt │ │ │ │ ├── RequestTest.kt │ │ │ │ ├── HarTest.kt │ │ │ │ ├── ResponseTest.kt │ │ │ │ └── EntryTest.kt │ │ │ └── repository │ │ │ │ └── RepositoryProviderTest.kt │ │ └── support │ │ │ ├── FileFactoryTest.kt │ │ │ ├── JsonConverterTest.kt │ │ │ ├── TransactionDetailsSharableTest.kt │ │ │ ├── TransactionListDetailsSharableTest.kt │ │ │ ├── OkioUtilsTest.kt │ │ │ ├── LiveDataCombineLatestTest.kt │ │ │ ├── LimitingSourceTest.kt │ │ │ ├── LiveDataDistinctUntilChangedTest.kt │ │ │ └── RequestProcessorTest.kt │ │ └── util │ │ ├── ClientFactory.kt │ │ ├── NoLoggerRule.kt │ │ ├── TestUtils.kt │ │ ├── ChuckerInterceptorDelegate.kt │ │ └── TestTransactionFactory.kt │ └── main │ ├── res │ ├── xml │ │ └── chucker_provider_paths.xml │ ├── mipmap-hdpi │ │ └── chucker_ic_launcher.png │ ├── mipmap-xhdpi │ │ └── chucker_ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── chucker_ic_launcher.png │ ├── mipmap-xxxhdpi │ │ └── chucker_ic_launcher.png │ ├── values │ │ ├── chucker_ic_launcher_background.xml │ │ ├── public.xml │ │ ├── dimens.xml │ │ ├── colors.xml │ │ └── styles.xml │ ├── color │ │ └── chucker_fab_background_colour.xml │ ├── mipmap-anydpi-v26 │ │ └── chucker_ic_launcher.xml │ ├── drawable │ │ ├── chucker_ic_delete_white.xml │ │ ├── chucker_ic_arrow_down.xml │ │ ├── chucker_ic_save_white.xml │ │ ├── chucker_ic_transaction_notification.xml │ │ ├── chucker_ic_search_white.xml │ │ ├── chucker_ic_decoded_url_white.xml │ │ ├── chucker_ic_https.xml │ │ ├── chucker_ic_http.xml │ │ ├── chucker_ic_launcher_foreground.xml │ │ ├── chucker_ic_share_white.xml │ │ ├── chucker_ic_encoded_url_white.xml │ │ ├── chucker_empty_payload.xml │ │ └── chucker_ic_graphql.xml │ ├── layout │ │ ├── chucker_transaction_item_headers.xml │ │ ├── chucker_transaction_item_body_line.xml │ │ ├── chucker_transaction_item_image.xml │ │ ├── chucker_activity_transaction.xml │ │ └── chucker_activity_main.xml │ ├── menu │ │ ├── chucker_transactions_list.xml │ │ └── chucker_transaction.xml │ └── values-night │ │ └── colors.xml │ ├── kotlin │ └── com │ │ └── chuckerteam │ │ └── chucker │ │ ├── internal │ │ ├── ui │ │ │ ├── transaction │ │ │ │ ├── PayloadType.kt │ │ │ │ ├── ProtocolResources.kt │ │ │ │ ├── TransactionPagerAdapter.kt │ │ │ │ └── TransactionViewModel.kt │ │ │ ├── BaseChuckerActivity.kt │ │ │ └── MainViewModel.kt │ │ ├── data │ │ │ ├── model │ │ │ │ └── DialogData.kt │ │ │ ├── entity │ │ │ │ ├── HttpHeader.kt │ │ │ │ └── HttpTransactionTuple.kt │ │ │ ├── har │ │ │ │ ├── Har.kt │ │ │ │ ├── log │ │ │ │ │ ├── Browser.kt │ │ │ │ │ ├── Creator.kt │ │ │ │ │ ├── page │ │ │ │ │ │ └── PageTimings.kt │ │ │ │ │ ├── entry │ │ │ │ │ │ ├── Cache.kt │ │ │ │ │ │ ├── cache │ │ │ │ │ │ │ └── SecondaryRequest.kt │ │ │ │ │ │ ├── request │ │ │ │ │ │ │ ├── postdata │ │ │ │ │ │ │ │ └── Params.kt │ │ │ │ │ │ │ ├── QueryString.kt │ │ │ │ │ │ │ └── PostData.kt │ │ │ │ │ │ ├── Header.kt │ │ │ │ │ │ ├── Timings.kt │ │ │ │ │ │ ├── response │ │ │ │ │ │ │ └── Content.kt │ │ │ │ │ │ ├── Response.kt │ │ │ │ │ │ └── Request.kt │ │ │ │ │ ├── Page.kt │ │ │ │ │ └── Entry.kt │ │ │ │ └── Log.kt │ │ │ ├── room │ │ │ │ ├── ChuckerDatabase.kt │ │ │ │ └── HttpTransactionDao.kt │ │ │ └── repository │ │ │ │ ├── HttpTransactionRepository.kt │ │ │ │ ├── RepositoryProvider.kt │ │ │ │ └── HttpTransactionDatabaseRepository.kt │ │ └── support │ │ │ ├── CacheDirectoryProvider.kt │ │ │ ├── ChuckerFileProvider.kt │ │ │ ├── TransactionDetailsHarSharable.kt │ │ │ ├── ClearDatabaseJobIntentServiceReceiver.kt │ │ │ ├── JsonConverter.kt │ │ │ ├── LimitingSource.kt │ │ │ ├── ContextExt.kt │ │ │ ├── TransactionDiffCallback.kt │ │ │ ├── Logger.kt │ │ │ ├── FileFactory.kt │ │ │ ├── TransactionListDetailsSharable.kt │ │ │ ├── HarUtils.kt │ │ │ ├── PlainTextDecoder.kt │ │ │ ├── DepletingSource.kt │ │ │ ├── ClearDatabaseService.kt │ │ │ ├── OkioUtils.kt │ │ │ ├── BitmapUtils.kt │ │ │ ├── TransactionCurlCommandSharable.kt │ │ │ ├── TeeSource.kt │ │ │ ├── FormattedUrl.kt │ │ │ ├── LiveDataUtils.kt │ │ │ ├── ChessboardDrawable.kt │ │ │ ├── Sharable.kt │ │ │ ├── SearchHighlightUtil.kt │ │ │ ├── OkHttpUtils.kt │ │ │ └── TransactionDetailsSharable.kt │ │ └── api │ │ ├── ExportFormat.kt │ │ ├── BodyDecoder.kt │ │ └── Chucker.kt │ └── AndroidManifest.xml ├── sample ├── debug.keystore ├── src │ ├── main │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── chuckerteam │ │ │ │ └── chucker │ │ │ │ └── sample │ │ │ │ ├── HttpTask.kt │ │ │ │ ├── InterceptorTypeSelector.kt │ │ │ │ ├── InterceptorType.kt │ │ │ │ ├── PokemonProtoBodyDecoder.kt │ │ │ │ ├── DummyImageHttpTask.kt │ │ │ │ ├── PostmanEchoHttpTask.kt │ │ │ │ ├── GraphQlTask.kt │ │ │ │ └── OkHttpUtils.kt │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── proto │ │ │ └── com │ │ │ │ └── chuckerteam │ │ │ │ └── chucker │ │ │ │ └── sample │ │ │ │ └── pokemon.proto │ │ ├── graphql │ │ │ └── com │ │ │ │ └── chuckerteam │ │ │ │ └── chucker │ │ │ │ └── sample │ │ │ │ └── Search.graphql │ │ └── AndroidManifest.xml │ └── debug │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── xml │ │ └── network_security_config.xml ├── proguard-rules.pro └── build.gradle ├── assets ├── chucker-http.gif ├── ic_launcher-web.png ├── chucker-multiwindow.gif └── generate_gif.sh ├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── kotlin-static-analysis.gradle └── gradle-mvn-push.gradle ├── .editorconfig ├── library-no-op ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── chuckerteam │ │ └── chucker │ │ └── api │ │ ├── ExportFormat.kt │ │ ├── BodyDecoder.kt │ │ ├── RetentionManager.kt │ │ ├── Chucker.kt │ │ ├── ChuckerCollector.kt │ │ └── ChuckerInterceptor.kt └── build.gradle ├── docs ├── README.md └── migrating-from-2.0.md ├── downloadApolloSchema.sh ├── pre-commit ├── detekt-config.yml ├── gradle.properties └── gradlew.bat /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ChuckerTeam/default-reviewers 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':library', ':library-no-op' 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ChuckerTeam 2 | open_collective: chucker 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.chuckerteam.chucker.internal.data.entity.HttpTransaction { *; } -------------------------------------------------------------------------------- /sample/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/debug.keystore -------------------------------------------------------------------------------- /assets/chucker-http.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/assets/chucker-http.gif -------------------------------------------------------------------------------- /assets/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/assets/ic_launcher-web.png -------------------------------------------------------------------------------- /assets/chucker-multiwindow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/assets/chucker-multiwindow.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea 3 | .gradle 4 | /local.properties 5 | .DS_Store 6 | **/build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /library/src/test/resources/sample_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/library/src/test/resources/sample_image.png -------------------------------------------------------------------------------- /library/src/main/res/xml/chucker_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/HttpTask.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | interface HttpTask { 4 | fun run() 5 | } 6 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /library/src/main/res/mipmap-hdpi/chucker_ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/library/src/main/res/mipmap-hdpi/chucker_ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /library/src/main/res/mipmap-xhdpi/chucker_ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/library/src/main/res/mipmap-xhdpi/chucker_ic_launcher.png -------------------------------------------------------------------------------- /library/src/main/res/mipmap-xxhdpi/chucker_ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/library/src/main/res/mipmap-xxhdpi/chucker_ic_launcher.png -------------------------------------------------------------------------------- /library/src/main/res/mipmap-xxxhdpi/chucker_ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingarajsankaravelu/chucker/develop/library/src/main/res/mipmap-xxxhdpi/chucker_ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #01579B 4 | -------------------------------------------------------------------------------- /library/src/main/res/values/chucker_ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #01579B 4 | -------------------------------------------------------------------------------- /library/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/PayloadType.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.ui.transaction 2 | 3 | internal enum class PayloadType { 4 | REQUEST, 5 | RESPONSE 6 | } 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #01579b 4 | #002f6c 5 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/InterceptorTypeSelector.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | class InterceptorTypeSelector : InterceptorType.Provider { 4 | override var value = InterceptorType.APPLICATION 5 | } 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #81d4fa 4 | #121212 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /sample/src/main/proto/com/chuckerteam/chucker/sample/pokemon.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.chuckerteam.chucker.sample; 4 | 5 | option java_package = "com.chuckerteam.chucker.sample"; 6 | 7 | message Pokemon { 8 | string name = 1; 9 | int32 level = 2; 10 | } 11 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/model/DialogData.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.model 2 | 3 | internal data class DialogData( 4 | val title: String, 5 | val message: String, 6 | val positiveButtonText: String?, 7 | val negativeButtonText: String? 8 | ) 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_size = 4 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | ktlint_disabled_rules=function-start-of-body-spacing 8 | 9 | [*.yml] 10 | indent_size = 2 11 | 12 | [**/test/**.kt] 13 | ktlint_ignore_back_ticked_identifier=true -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpHeader.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | internal data class HttpHeader( 6 | @SerializedName("name") val name: String, 7 | @SerializedName("value") val value: String 8 | ) 9 | -------------------------------------------------------------------------------- /library/src/main/res/color/chucker_fab_background_colour.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/debug/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /library/src/main/res/mipmap-anydpi-v26/chucker_ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 16dp 5 | 500dp 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/CacheDirectoryProvider.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import java.io.File 4 | 5 | /** 6 | * An interface that returns a reference to a cache directory where temporary files can be 7 | * saved. 8 | */ 9 | internal fun interface CacheDirectoryProvider { 10 | fun provide(): File? 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/api/ExportFormat.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | /** 4 | * The supported export format for transactions file exports. 5 | */ 6 | public enum class ExportFormat(public val extension: String) { 7 | /** LOG Format with txt extension */ 8 | LOG("txt"), 9 | 10 | /** HAR format with har extension */ 11 | HAR("har") 12 | } 13 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/ExportFormat.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | /** 4 | * The supported export format for transactions file exports. 5 | */ 6 | public enum class ExportFormat(public val extension: String) { 7 | /** LOG Format with txt extension */ 8 | LOG("txt"), 9 | 10 | /** HAR format with har extension */ 11 | HAR("har") 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/ChuckerFileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import androidx.core.content.FileProvider 4 | 5 | /** 6 | * We need our own subclass so we don't conflict with other [FileProvider]s 7 | * See: https://github.com/ChuckerTeam/chucker/issues/409 8 | */ 9 | internal class ChuckerFileProvider : FileProvider() 10 | -------------------------------------------------------------------------------- /library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 16dp 6 | 32dp 7 | 64dp 8 | 9 | 56dp 10 | -------------------------------------------------------------------------------- /sample/src/main/graphql/com/chuckerteam/chucker/sample/Search.graphql: -------------------------------------------------------------------------------- 1 | query SearchCharacters( 2 | $searchTerm: String, 3 | ) { 4 | characters(page: 1, filter: { name: $searchTerm}) { 5 | info { 6 | count 7 | } 8 | results { 9 | name 10 | } 11 | } 12 | location(id: 1) { 13 | id 14 | } 15 | episodesByIds(ids: [1, 2]) { 16 | id 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TransactionDetailsHarSharable.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import okio.Buffer 5 | import okio.Source 6 | 7 | internal class TransactionDetailsHarSharable( 8 | private val content: String 9 | ) : Sharable { 10 | override fun toSharableContent(context: Context): Source = Buffer().writeUtf8("$content\n") 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_delete_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_save_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chucker_transaction_item_headers.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/ClearDatabaseJobIntentServiceReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | internal class ClearDatabaseJobIntentServiceReceiver : BroadcastReceiver() { 8 | 9 | override fun onReceive(context: Context, intent: Intent) { 10 | ClearDatabaseService.enqueueWork(context, intent) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: Validate Gradle Wrapper 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | pull_request: 7 | branches: 8 | - '*' 9 | workflow_dispatch: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | validation: 15 | name: Validation 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout latest code 19 | uses: actions/checkout@v4 20 | - name: Validate Gradle Wrapper 21 | uses: gradle/wrapper-validation-action@v1 22 | 23 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_transaction_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chucker_transaction_item_body_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/PostDataTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.util.HarTestUtils 4 | import com.google.common.truth.Truth.assertThat 5 | import org.junit.Test 6 | 7 | internal class PostDataTest { 8 | @Test 9 | fun `post data is created correctly with mime type`() { 10 | val postData = HarTestUtils.createPostData("POST") 11 | 12 | assertThat(postData?.mimeType).isEqualTo("application/json") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/BodyDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import okhttp3.Request 4 | import okhttp3.Response 5 | import okio.ByteString 6 | import okio.IOException 7 | 8 | /** 9 | * No-op declaration 10 | */ 11 | public interface BodyDecoder { 12 | @Throws(IOException::class) 13 | public fun decodeRequest(request: Request, body: ByteString): String? 14 | 15 | @Throws(IOException::class) 16 | public fun decodeResponse(response: Response, body: ByteString): String? 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/Har.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.log.Creator 5 | import com.google.gson.annotations.SerializedName 6 | 7 | internal data class Har( 8 | @SerializedName("log") val log: Log 9 | ) { 10 | constructor(transactions: List, creator: Creator) : this( 11 | log = Log(transactions, creator) 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/Browser.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#browser 6 | // http://www.softwareishard.com/blog/har-12-spec/#browser 7 | internal data class Browser( 8 | @SerializedName("name") val name: String, 9 | @SerializedName("version") val version: String, 10 | @SerializedName("comment") val comment: String? = null 11 | ) 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/Creator.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#creator 6 | // http://www.softwareishard.com/blog/har-12-spec/#creator 7 | internal data class Creator( 8 | @SerializedName("name") val name: String, 9 | @SerializedName("version") val version: String, 10 | @SerializedName("comment") val comment: String? = null 11 | ) 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Chucker Docs 2 | 3 | Please find here the documentation for the Chucker Project. 4 | 5 | ## Changelogs 6 | 7 | The full changelog is available in the [CHANGELOG](/CHANGELOG.md) page. Please visit also the [release page](https://github.com/ChuckerTeam/chucker/releases) as shorter release notes are published also there. 8 | 9 | ## Migration guides 10 | 11 | If you need support migrating to Chucker from other libraries please read: 12 | 13 | * [Migrating from Chucker 2.x to 3.x](migrating-from-2.0.md) 14 | * [Migrating from Chuck](migrating-from-chuck.md) -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/ProtocolResources.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.ui.transaction 2 | 3 | import androidx.annotation.ColorRes 4 | import androidx.annotation.DrawableRes 5 | import com.chuckerteam.chucker.R 6 | 7 | internal sealed class ProtocolResources(@DrawableRes val icon: Int, @ColorRes val color: Int) { 8 | class Http : ProtocolResources(R.drawable.chucker_ic_http, R.color.chucker_color_error) 9 | class Https : ProtocolResources(R.drawable.chucker_ic_https, R.color.chucker_color_primary) 10 | } 11 | -------------------------------------------------------------------------------- /downloadApolloSchema.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # GraphQL Schema Update 4 | # Open Android Studio terminal 5 | # Run command `./downloadApolloSchema.sh` 6 | # Format the file (`ctrl+option+i` && `option+cmd+L`), if not formatted 7 | # Replace `"null"` to `null` 8 | # Go to the top of the file and click the play icon ▶️ "Generate GraphQL SDL schema file". (It normally can be visible on Android Studio and IntelliJ IDE) 9 | # Be happy. 10 | 11 | ./gradlew downloadApolloSchema --endpoint="https://rickandmortyapi.com/graphql" --schema="sample/src/main/graphql/com/chuckerteam/chucker/sample/schema.json" 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/page/PageTimings.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.page 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#pagetimings 6 | // http://www.softwareishard.com/blog/har-12-spec/#pageTimings 7 | internal data class PageTimings( 8 | @SerializedName("onContentLoad") val onContentLoad: Long? = null, 9 | @SerializedName("onLoad") val onLoad: Long? = null, 10 | @SerializedName("comment") val comment: String? = null 11 | ) 12 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/FileFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.io.TempDir 6 | import java.io.File 7 | 8 | internal class FileFactoryTest { 9 | @TempDir lateinit var tempDir: File 10 | 11 | @Test 12 | fun `file is created if parent does not exist`() { 13 | assertThat(tempDir.deleteRecursively()).isTrue() 14 | 15 | assertThat(FileFactory.create(tempDir)!!.isFile).isTrue() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/JsonConverter.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | 6 | internal object JsonConverter { 7 | 8 | val nonNullSerializerInstance: Gson by lazy { 9 | GsonBuilder() 10 | .disableHtmlEscaping() 11 | .setPrettyPrinting() 12 | .create() 13 | } 14 | 15 | val instance: Gson by lazy { 16 | nonNullSerializerInstance.newBuilder() 17 | .serializeNulls() 18 | .create() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_search_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_decoded_url_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_https.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chucker_transaction_item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/InterceptorType.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | import okhttp3.Interceptor 4 | 5 | enum class InterceptorType { 6 | APPLICATION, 7 | NETWORK 8 | ; 9 | 10 | interface Provider { 11 | val value: InterceptorType 12 | } 13 | } 14 | 15 | fun Interceptor.activeForType( 16 | activeType: InterceptorType, 17 | typeProvider: InterceptorType.Provider 18 | ) = Interceptor { chain -> 19 | if (activeType == typeProvider.value) { 20 | intercept(chain) 21 | } else { 22 | chain.proceed(chain.request()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_http.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/RetentionManager.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * No-op implementation. 7 | */ 8 | @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER") 9 | public class RetentionManager @JvmOverloads constructor( 10 | context: Context, 11 | retentionPeriod: Any? = null 12 | ) { 13 | 14 | @Synchronized 15 | public fun doMaintenance() { 16 | // Empty method for the library-no-op artifact 17 | } 18 | 19 | public enum class Period { 20 | ONE_HOUR, 21 | ONE_DAY, 22 | ONE_WEEK, 23 | FOREVER 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/LimitingSource.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okio.Buffer 4 | import okio.ForwardingSource 5 | import okio.Source 6 | 7 | internal class LimitingSource( 8 | delegate: Source, 9 | private val bytesCountThreshold: Long 10 | ) : ForwardingSource(delegate) { 11 | private var bytesRead = 0L 12 | val isThresholdReached get() = bytesRead >= bytesCountThreshold 13 | 14 | override fun read(sink: Buffer, byteCount: Long) = if (!isThresholdReached) { 15 | super.read(sink, byteCount).also { bytesRead += it } 16 | } else { 17 | -1L 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/PokemonProtoBodyDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | import com.chuckerteam.chucker.api.BodyDecoder 4 | import okhttp3.Request 5 | import okio.ByteString 6 | 7 | internal class PokemonProtoBodyDecoder : BodyDecoder { 8 | override fun decodeRequest(request: Request, body: ByteString): String? { 9 | return if (request.url.host.contains("postman", ignoreCase = true)) { 10 | Pokemon.ADAPTER.decode(body).toString() 11 | } else { 12 | null 13 | } 14 | } 15 | 16 | override fun decodeResponse(response: okhttp3.Response, body: ByteString): String? = null 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Cache.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.entry.cache.SecondaryRequest 4 | import com.google.gson.annotations.SerializedName 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#cache 7 | // http://www.softwareishard.com/blog/har-12-spec/#cache 8 | internal data class Cache( 9 | @SerializedName("afterRequest") val afterRequest: SecondaryRequest? = null, 10 | @SerializedName("beforeRequest") val beforeRequest: SecondaryRequest? = null, 11 | @SerializedName("comment") val comment: String? = null 12 | ) 13 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/DummyImageHttpTask.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.Request 5 | 6 | class DummyImageHttpTask( 7 | private val client: OkHttpClient 8 | ) : HttpTask { 9 | override fun run() { 10 | getImage(colorHex = "fff") 11 | getImage(colorHex = "000") 12 | } 13 | 14 | private fun getImage(colorHex: String) { 15 | val request = Request.Builder() 16 | .url("https://dummyimage.com/200x200/$colorHex/$colorHex.png") 17 | .get() 18 | .build() 19 | client.newCall(request).enqueue(ReadBytesCallback()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/Chucker.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | 6 | /** 7 | * No-op implementation. 8 | */ 9 | @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER") 10 | public object Chucker { 11 | 12 | @Suppress("MayBeConst ") // https://github.com/ChuckerTeam/chucker/pull/169#discussion_r362341353 13 | public val isOp: Boolean = false 14 | 15 | @JvmStatic 16 | public fun getLaunchIntent(context: Context): Intent = Intent() 17 | 18 | @JvmStatic 19 | public fun dismissNotifications(context: Context) { 20 | // Empty method for the library-no-op artifact 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Updates for Github Actions used in the repo 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | reviewers: 12 | - "cortinico" 13 | - "vbuberen" 14 | # Updates for Gradle dependencies used in the app 15 | - package-ecosystem: gradle 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | open-pull-requests-limit: 10 20 | reviewers: 21 | - "cortinico" 22 | - "vbuberen" 23 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/cache/SecondaryRequest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry.cache 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#beforerequest--afterrequest 6 | // http://www.softwareishard.com/blog/har-12-spec/#cache 7 | internal data class SecondaryRequest( 8 | @SerializedName("expires") val expires: String? = null, 9 | @SerializedName("lastAccess") val lastAccess: String, 10 | @SerializedName("eTag") val eTag: String, 11 | @SerializedName("hitCount") val hitCount: Int, 12 | @SerializedName("comment") val comment: String? = null 13 | ) 14 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/request/postdata/Params.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry.request.postdata 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#params 6 | // http://www.softwareishard.com/blog/har-12-spec/#params 7 | internal data class Params( 8 | @SerializedName("name") val name: String, 9 | @SerializedName("value") val value: String? = null, 10 | @SerializedName("fileName") val fileName: String? = null, 11 | @SerializedName("contentType") val contentType: String? = null, 12 | @SerializedName("comment") val comment: String? = null 13 | ) 14 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Running pre-commit checks..." 3 | 4 | OUTPUT="/tmp/analysis-result" 5 | ./gradlew detekt ktlintCheck > ${OUTPUT} 6 | EXIT_CODE=$? 7 | if [ ${EXIT_CODE} -ne 0 ]; then 8 | cat ${OUTPUT} 9 | rm ${OUTPUT} 10 | echo "*********************************************" 11 | echo " Checks Failed! " 12 | echo " Resolve found issues before committing " 13 | echo "*********************************************" 14 | exit ${EXIT_CODE} 15 | else 16 | rm ${OUTPUT} 17 | echo "*********************************************" 18 | echo " Checks Passed Successfully! " 19 | echo "*********************************************" 20 | fi 21 | -------------------------------------------------------------------------------- /.github/workflows/close-and-release-repository.yaml: -------------------------------------------------------------------------------- 1 | name: Close and Release Repository 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish: 6 | runs-on: [ubuntu-latest] 7 | 8 | steps: 9 | - name: Checkout Repo 10 | uses: actions/checkout@v4 11 | 12 | - name: Setup Java 13 | uses: actions/setup-java@v3 14 | with: 15 | distribution: 'zulu' 16 | java-version: '17' 17 | 18 | - name: Close and Release Staging Repository 19 | run: ./gradlew closeAndReleaseStagingRepository 20 | env: 21 | ORG_GRADLE_PROJECT_NEXUS_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_USERNAME }} 22 | ORG_GRADLE_PROJECT_NEXUS_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_PASSWORD }} 23 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/Page.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.page.PageTimings 4 | import com.google.gson.annotations.SerializedName 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#pages 7 | // http://www.softwareishard.com/blog/har-12-spec/#pages 8 | internal data class Page( 9 | @SerializedName("startedDateTime") val startedDateTime: String, 10 | @SerializedName("id") val id: String, 11 | @SerializedName("title") val title: String, 12 | @SerializedName("pageTimings") val pageTimings: PageTimings, 13 | @SerializedName("comment") val comment: String? = null 14 | ) 15 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Header.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpHeader 4 | import com.google.gson.annotations.SerializedName 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#headers 7 | // http://www.softwareishard.com/blog/har-12-spec/#headers 8 | internal data class Header( 9 | @SerializedName("name") val name: String, 10 | @SerializedName("value") val value: String, 11 | @SerializedName("comment") val comment: String? = null 12 | ) { 13 | constructor(header: HttpHeader) : this( 14 | name = header.name, 15 | value = header.value 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /gradle/kotlin-static-analysis.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.gitlab.arturbosch.detekt' 2 | apply plugin: 'org.jlleitschuh.gradle.ktlint' 3 | 4 | android.sourceSets.configureEach { 5 | java.srcDirs("src/$name/kotlin") 6 | } 7 | 8 | ktlint { 9 | debug = false 10 | verbose = true 11 | android = false 12 | outputToConsole = true 13 | ignoreFailures = false 14 | enableExperimentalRules = true 15 | kotlinScriptAdditionalPaths { 16 | include fileTree("scripts/") 17 | } 18 | filter { 19 | exclude { element -> element.file.path.contains("generated/") } 20 | include("**/kotlin/**") 21 | } 22 | } 23 | 24 | detekt { 25 | config = files("../detekt-config.yml") 26 | buildUponDefaultConfig = true 27 | } 28 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerCollector.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | 6 | /** 7 | * No-op implementation. 8 | */ 9 | @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER") 10 | public class ChuckerCollector @JvmOverloads constructor( 11 | context: Context, 12 | public var showNotification: Boolean = true, 13 | retentionPeriod: RetentionManager.Period = RetentionManager.Period.ONE_WEEK 14 | ) { 15 | @Suppress("FunctionOnlyReturningConstant") 16 | public fun writeTransactions( 17 | context: Context, 18 | startTimestamp: Long?, 19 | exportFormat: ExportFormat = ExportFormat.LOG 20 | ): Uri? = null 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_share_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/util/ClientFactory.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.util 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.OkHttpClient 5 | 6 | internal enum class ClientFactory { 7 | APPLICATION { 8 | override fun create(interceptor: Interceptor): OkHttpClient { 9 | return OkHttpClient.Builder() 10 | .addInterceptor(interceptor) 11 | .build() 12 | } 13 | }, 14 | NETWORK { 15 | override fun create(interceptor: Interceptor): OkHttpClient { 16 | return OkHttpClient.Builder() 17 | .addNetworkInterceptor(interceptor) 18 | .build() 19 | } 20 | }; 21 | 22 | abstract fun create(interceptor: Interceptor): OkHttpClient 23 | } 24 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jgilfelt/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -dontwarn okio.** 19 | -dontwarn retrofit2.** 20 | -------------------------------------------------------------------------------- /detekt-config.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | weights: 4 | # complexity: 2 5 | # LongParameterList: 1 6 | # style: 1 7 | # comments: 1 8 | 9 | complexity: 10 | active: true 11 | CyclomaticComplexMethod: 12 | active: true 13 | threshold: 16 14 | ComplexCondition: 15 | active: true 16 | threshold: 5 17 | LongMethod: 18 | active: true 19 | excludes: ['**/test/**', '**/androidTest/**'] 20 | LongParameterList: 21 | active: true 22 | ignoreDefaultParameters: true 23 | TooManyFunctions: 24 | active: true 25 | ignoreOverridden: true 26 | 27 | style: 28 | ReturnCount: 29 | active: true 30 | max: 5 31 | excludedFunctions: ["equals"] 32 | excludeLabeled: false 33 | excludeReturnFromLambda: true 34 | UnusedImports: 35 | active: true 36 | -------------------------------------------------------------------------------- /assets/generate_gif.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Require 3 files: 3 | # - chucker-http.mp4 4 | # - chucker-error.mp4 5 | # - chucker-multiwindow.mp4 6 | 7 | ffmpeg -y -i chucker-http.mp4 -vf fps=10,scale=310:-1:flags=lanczos,palettegen palette.png 8 | ffmpeg -i chucker-http.mp4 -i palette.png -filter_complex "fps=10,scale=310:-1:flags=lanczos[x];[x][1:v]paletteuse" chucker-http.gif 9 | 10 | ffmpeg -y -i chucker-error.mp4 -vf fps=10,scale=310:-1:flags=lanczos,palettegen palette.png 11 | ffmpeg -i chucker-error.mp4 -i palette.png -filter_complex "fps=10,scale=310:-1:flags=lanczos[x];[x][1:v]paletteuse" chucker-error.gif 12 | 13 | ffmpeg -y -i chucker-multiwindow.mp4 -vf fps=10,scale=720:-1:flags=lanczos,palettegen palette.png 14 | ffmpeg -i chucker-multiwindow.mp4 -i palette.png -filter_complex "fps=10,scale=720:-1:flags=lanczos[x];[x][1:v]paletteuse" chucker-multiwindow.gif 15 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.internal.data.model.DialogData 5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 6 | 7 | internal fun Context.showDialog( 8 | dialogData: DialogData, 9 | onPositiveClick: (() -> Unit)?, 10 | onNegativeClick: (() -> Unit)? 11 | ) { 12 | MaterialAlertDialogBuilder(this) 13 | .setTitle(dialogData.title) 14 | .setMessage(dialogData.message) 15 | .setPositiveButton(dialogData.positiveButtonText) { _, _ -> 16 | onPositiveClick?.invoke() 17 | } 18 | .setNegativeButton(dialogData.negativeButtonText) { _, _ -> 19 | onNegativeClick?.invoke() 20 | } 21 | .show() 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ## :writing_hand: Describe the bug 8 | 9 | 10 | ## :bomb: Steps to reproduce 11 | 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | ## :wrench: Expected behavior 18 | 19 | 20 | ## :camera: Screenshots 21 | 22 | 23 | ## :iphone: Tech info 24 | - Device: 25 | - OS: 26 | - Chucker version: 27 | 28 | ## :page_facing_up: Additional context 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | ## :warning: Is your feature request related to a problem? Please describe 8 | 9 | 10 | ## :bulb: Describe the solution you'd like 11 | 12 | 13 | ## :bar_chart: Describe alternatives you've considered 14 | 15 | 16 | ## :page_facing_up: Additional context 17 | 18 | 19 | ## :raising_hand: Do you want to develop this feature yourself? 20 | 21 | - [ ] Yes 22 | - [ ] No 23 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TransactionDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple 5 | 6 | internal object TransactionDiffCallback : DiffUtil.ItemCallback() { 7 | override fun areItemsTheSame(oldItem: HttpTransactionTuple, newItem: HttpTransactionTuple): Boolean { 8 | return oldItem.id == newItem.id 9 | } 10 | 11 | override fun areContentsTheSame(oldItem: HttpTransactionTuple, newItem: HttpTransactionTuple): Boolean { 12 | return oldItem == newItem 13 | } 14 | 15 | // Overriding function is empty on purpose to avoid flickering by default animator 16 | override fun getChangePayload(oldItem: HttpTransactionTuple, newItem: HttpTransactionTuple) = Unit 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_encoded_url_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.chuckerteam.chucker.api.Chucker 4 | 5 | internal interface Logger { 6 | fun info(message: String, throwable: Throwable? = null) 7 | 8 | fun warn(message: String, throwable: Throwable? = null) 9 | 10 | fun error(message: String, throwable: Throwable? = null) 11 | 12 | companion object : Logger { 13 | override fun info(message: String, throwable: Throwable?) { 14 | Chucker.logger.info(message, throwable) 15 | } 16 | 17 | override fun warn(message: String, throwable: Throwable?) { 18 | Chucker.logger.warn(message, throwable) 19 | } 20 | 21 | override fun error(message: String, throwable: Throwable?) { 22 | Chucker.logger.error(message, throwable) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/JsonConverterTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | internal class JsonConverterTest { 7 | 8 | @Test 9 | fun `JSON converter is a singleton`() { 10 | val instance1 = JsonConverter.instance 11 | val instance2 = JsonConverter.instance 12 | 13 | assertThat(instance1).isEqualTo(instance2) 14 | } 15 | 16 | @Test 17 | fun `JSON object has null values serialized`() { 18 | val json = JsonConverter.instance.toJson(NullTestClass(null)) 19 | assertThat(json).isEqualTo( 20 | """ 21 | { 22 | "string": null 23 | } 24 | """.trimIndent() 25 | ) 26 | } 27 | 28 | inner class NullTestClass(var string: String?) 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/ChuckerDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.room 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 8 | 9 | @Database(entities = [HttpTransaction::class], version = 9, exportSchema = false) 10 | internal abstract class ChuckerDatabase : RoomDatabase() { 11 | 12 | abstract fun transactionDao(): HttpTransactionDao 13 | 14 | companion object { 15 | private const val DB_NAME = "chucker.db" 16 | 17 | fun create(applicationContext: Context): ChuckerDatabase { 18 | return Room.databaseBuilder(applicationContext, ChuckerDatabase::class.java, DB_NAME) 19 | .fallbackToDestructiveMigration() 20 | .build() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## :camera: Screenshots 2 | 3 | 4 | ## :page_facing_up: Context 5 | 6 | 7 | ## :pencil: Changes 8 | 9 | 10 | 11 | ## :paperclip: Related PR 12 | 13 | 14 | ## :no_entry_sign: Breaking 15 | 16 | 17 | ## :hammer_and_wrench: How to test 18 | 19 | 20 | ## :stopwatch: Next steps 21 | 22 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/ContentTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.util.HarTestUtils 4 | import com.google.common.truth.Truth.assertThat 5 | import org.junit.Test 6 | 7 | internal class ContentTest { 8 | @Test 9 | fun `content is created correctly with size`() { 10 | val content = HarTestUtils.createContent("GET") 11 | 12 | assertThat(content.size).isEqualTo(1000) 13 | } 14 | 15 | @Test 16 | fun `content is created correctly with mime type`() { 17 | val content = HarTestUtils.createContent("GET") 18 | 19 | assertThat(content.mimeType).isEqualTo("application/json") 20 | } 21 | 22 | @Test 23 | fun `content is created correctly with fields and values`() { 24 | val content = HarTestUtils.createContent("GET") 25 | 26 | assertThat(content.text).isEqualTo("""{"field": "value"}""") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_empty_payload.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/request/QueryString.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry.request 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import okhttp3.HttpUrl 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#querystring 7 | // http://www.softwareishard.com/blog/har-12-spec/#queryString 8 | internal data class QueryString( 9 | @SerializedName("name") val name: String, 10 | @SerializedName("value") val value: String, 11 | @SerializedName("comment") val comment: String? = null 12 | ) { 13 | companion object { 14 | fun fromUrl(url: HttpUrl): List { 15 | return List(url.querySize) { index -> 16 | QueryString( 17 | name = url.queryParameterName(index), 18 | value = url.queryParameterValue(index).orEmpty() 19 | ) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/FileFactory.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import java.io.File 4 | import java.io.IOException 5 | import java.util.concurrent.atomic.AtomicLong 6 | 7 | internal object FileFactory { 8 | private val uniqueIdGenerator = AtomicLong() 9 | 10 | fun create(directory: File) = create(directory, fileName = "chucker-${uniqueIdGenerator.getAndIncrement()}") 11 | 12 | fun create(directory: File, fileName: String): File? = try { 13 | File(directory, fileName).apply { 14 | if (exists() && !delete()) { 15 | throw IOException("Failed to delete file $this") 16 | } 17 | parentFile?.mkdirs() 18 | if (!createNewFile()) { 19 | throw IOException("File $this already exists") 20 | } 21 | } 22 | } catch (e: IOException) { 23 | Logger.error("An error occurred while creating a file", e) 24 | null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/util/NoLoggerRule.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.util 2 | 3 | import com.chuckerteam.chucker.api.Chucker 4 | import com.chuckerteam.chucker.internal.support.Logger 5 | import org.junit.jupiter.api.extension.AfterAllCallback 6 | import org.junit.jupiter.api.extension.BeforeAllCallback 7 | import org.junit.jupiter.api.extension.ExtensionContext 8 | 9 | internal class NoLoggerRule : BeforeAllCallback, AfterAllCallback { 10 | private val defaultLogger = Chucker.logger 11 | 12 | override fun beforeAll(context: ExtensionContext) { 13 | Chucker.logger = object : Logger { 14 | override fun info(message: String, throwable: Throwable?) = Unit 15 | 16 | override fun warn(message: String, throwable: Throwable?) = Unit 17 | 18 | override fun error(message: String, throwable: Throwable?) = Unit 19 | } 20 | } 21 | 22 | override fun afterAll(context: ExtensionContext) { 23 | Chucker.logger = defaultLogger 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharable.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.R.string 5 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 6 | import okio.Buffer 7 | import okio.Source 8 | 9 | internal class TransactionListDetailsSharable( 10 | transactions: List, 11 | encodeUrls: Boolean 12 | ) : Sharable { 13 | private val transactions = transactions.map { TransactionDetailsSharable(it, encodeUrls) } 14 | 15 | override fun toSharableContent(context: Context): Source = Buffer().writeUtf8( 16 | transactions.joinToString( 17 | separator = "\n${context.getString(string.chucker_export_separator)}\n", 18 | prefix = "${context.getString(string.chucker_export_prefix)}\n", 19 | postfix = "\n${context.getString(string.chucker_export_postfix)}\n" 20 | ) { it.toSharableUtf8Content(context) } 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/request/PostData.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry.request 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.postdata.Params 5 | import com.google.gson.annotations.SerializedName 6 | 7 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#postdata 8 | // http://www.softwareishard.com/blog/har-12-spec/#postData 9 | // text and params fields are mutually exclusive. 10 | internal data class PostData( 11 | @SerializedName("mimeType") val mimeType: String, 12 | @SerializedName("params") val params: Params? = null, 13 | @SerializedName("text") val text: String? = null, 14 | @SerializedName("comment") val comment: String? = null 15 | ) { 16 | constructor(transaction: HttpTransaction) : this( 17 | mimeType = transaction.requestContentType ?: "application/octet-stream", 18 | text = transaction.requestBody 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/HarUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.Har 5 | import com.chuckerteam.chucker.internal.data.har.log.Creator 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | 9 | // http://www.softwareishard.com/blog/har-12-spec/ 10 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md 11 | internal object HarUtils { 12 | suspend fun harStringFromTransactions( 13 | transactions: List, 14 | name: String, 15 | version: String 16 | ): String = withContext(Dispatchers.Default) { 17 | JsonConverter.nonNullSerializerInstance 18 | .toJson(fromHttpTransactions(transactions, Creator(name, version))) 19 | } 20 | 21 | internal fun fromHttpTransactions(transactions: List, creator: Creator): Har { 22 | return Har(transactions, creator) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.chuckerteam.chucker.api.BodyDecoder 4 | import okhttp3.Headers 5 | import okhttp3.MediaType 6 | import okhttp3.Request 7 | import okhttp3.Response 8 | import okio.ByteString 9 | import kotlin.text.Charsets.UTF_8 10 | 11 | internal object PlainTextDecoder : BodyDecoder { 12 | override fun decodeRequest( 13 | request: Request, 14 | body: ByteString 15 | ) = body.tryDecodeAsPlainText(request.headers, request.body?.contentType()) 16 | 17 | override fun decodeResponse( 18 | response: Response, 19 | body: ByteString 20 | ) = body.tryDecodeAsPlainText(response.headers, response.body?.contentType()) 21 | 22 | private fun ByteString.tryDecodeAsPlainText( 23 | headers: Headers, 24 | contentType: MediaType? 25 | ) = if (headers.hasSupportedContentEncoding && isProbablyPlainText) { 26 | string(contentType?.charset() ?: UTF_8) 27 | } else { 28 | null 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/BaseChuckerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.ui 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider 7 | 8 | internal abstract class BaseChuckerActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | RepositoryProvider.initialize(applicationContext) 13 | } 14 | 15 | override fun onResume() { 16 | super.onResume() 17 | isInForeground = true 18 | } 19 | 20 | override fun onPause() { 21 | super.onPause() 22 | isInForeground = false 23 | } 24 | 25 | companion object { 26 | var isInForeground: Boolean = false 27 | private set 28 | } 29 | 30 | fun showToast(message: String, toastDuration: Int = Toast.LENGTH_SHORT) { 31 | Toast.makeText(this.applicationContext, message, toastDuration).show() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Chucker Sample App 3 | Interceptor type: 4 | Application 5 | Network 6 | Do HTTP activity 7 | Do GraphQL activity 8 | Launch Chucker directly 9 | Export to LOG file 10 | Export to HAR file 11 | Failed to export to file 12 | Transactions exported to: %s 13 | 14 | 15 | Welcome to Chucker Sample App 16 | You can use this sample app to do some HTTP networking and to see how Chucker can help you inspect your networking calls. 17 | 18 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/DepletingSource.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okio.Buffer 4 | import okio.ForwardingSource 5 | import okio.Source 6 | import okio.blackholeSink 7 | import okio.buffer 8 | import java.io.IOException 9 | 10 | internal class DepletingSource(delegate: Source) : ForwardingSource(delegate) { 11 | private var shouldDeplete = true 12 | 13 | override fun read(sink: Buffer, byteCount: Long) = try { 14 | val bytesRead = super.read(sink, byteCount) 15 | if (bytesRead == -1L) shouldDeplete = false 16 | bytesRead 17 | } catch (e: IOException) { 18 | shouldDeplete = false 19 | throw e 20 | } 21 | 22 | override fun close() { 23 | if (shouldDeplete) { 24 | try { 25 | delegate.buffer().readAll(blackholeSink()) 26 | } catch (e: IOException) { 27 | Logger.error("An error occurred while depleting the source", e) 28 | } 29 | } 30 | shouldDeplete = false 31 | 32 | super.close() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/ClearDatabaseService.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.app.JobIntentService 6 | import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider 7 | import kotlinx.coroutines.MainScope 8 | import kotlinx.coroutines.launch 9 | 10 | internal class ClearDatabaseService : JobIntentService() { 11 | private val scope = MainScope() 12 | 13 | override fun onHandleWork(intent: Intent) { 14 | RepositoryProvider.initialize(applicationContext) 15 | scope.launch { 16 | RepositoryProvider.transaction().deleteAllTransactions() 17 | NotificationHelper.clearBuffer() 18 | NotificationHelper(applicationContext).dismissNotifications() 19 | } 20 | } 21 | 22 | companion object { 23 | private const val CLEAN_DATABASE_JOB_ID = 123321 24 | 25 | fun enqueueWork(context: Context, work: Intent) { 26 | enqueueWork(context, ClearDatabaseService::class.java, CLEAN_DATABASE_JOB_ID, work) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/QueryStringTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.QueryString 4 | import com.google.common.truth.Truth.assertThat 5 | import okhttp3.HttpUrl.Companion.toHttpUrl 6 | import org.junit.Test 7 | 8 | internal class QueryStringTest { 9 | @Test 10 | fun `query string list is created correctly with url with queries`() { 11 | val url = "https://fake.url.com/path?query1=a&query2=b#the-fragment-part".toHttpUrl() 12 | val queryStringList = QueryString.fromUrl(url) 13 | assertThat(queryStringList).hasSize(2) 14 | assertThat(queryStringList[0]).isEqualTo(QueryString(name = "query1", value = "a")) 15 | assertThat(queryStringList[1]).isEqualTo(QueryString(name = "query2", value = "b")) 16 | } 17 | 18 | @Test 19 | fun `query string list is created correctly with url with no query`() { 20 | val url = "https://fake.url.com/path#the-fragment-part".toHttpUrl() 21 | val queryStringList = QueryString.fromUrl(url) 22 | assertThat(queryStringList).isEmpty() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Timings.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.google.gson.annotations.SerializedName 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#timings 7 | // http://www.softwareishard.com/blog/har-12-spec/#timings 8 | internal data class Timings( 9 | @SerializedName("blocked") val blocked: Long? = null, 10 | @SerializedName("dns") val dns: Long? = null, 11 | @SerializedName("ssl") val ssl: Long? = null, 12 | @SerializedName("connect") val connect: Long = 0, 13 | @SerializedName("send") val send: Long = 0, 14 | @SerializedName("wait") val wait: Long, 15 | @SerializedName("receive") val receive: Long = 0, 16 | @SerializedName("comment") val comment: String = "The information described by this object is incomplete." 17 | ) { 18 | constructor(transaction: HttpTransaction) : this( 19 | wait = transaction.tookMs ?: 0 20 | ) 21 | 22 | fun getTime(): Long { 23 | return (blocked ?: 0) + (dns ?: 0) + (ssl ?: 0) + connect + send + wait + receive 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/Log.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.log.Browser 5 | import com.chuckerteam.chucker.internal.data.har.log.Creator 6 | import com.chuckerteam.chucker.internal.data.har.log.Entry 7 | import com.chuckerteam.chucker.internal.data.har.log.Page 8 | import com.google.gson.annotations.SerializedName 9 | 10 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#log 11 | // http://www.softwareishard.com/blog/har-12-spec/#log 12 | internal data class Log( 13 | @SerializedName("version") val version: String = "1.2", 14 | @SerializedName("creator") val creator: Creator, 15 | @SerializedName("browser") val browser: Browser? = null, 16 | @SerializedName("pages") val pages: List? = null, 17 | @SerializedName("entries") val entries: List, 18 | @SerializedName("comment") val comment: String? = null 19 | ) { 20 | constructor(transactions: List, creator: Creator) : this( 21 | creator = creator, 22 | entries = transactions.map { Entry(it) } 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.ui.transaction 2 | 3 | import android.content.Context 4 | import androidx.fragment.app.Fragment 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.fragment.app.FragmentStatePagerAdapter 7 | import com.chuckerteam.chucker.R 8 | 9 | internal class TransactionPagerAdapter(context: Context, fm: FragmentManager) : 10 | FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 11 | 12 | private val titles = arrayOf( 13 | context.getString(R.string.chucker_overview), 14 | context.getString(R.string.chucker_request), 15 | context.getString(R.string.chucker_response) 16 | ) 17 | 18 | override fun getItem(position: Int): Fragment = when (position) { 19 | 0 -> TransactionOverviewFragment() 20 | 1 -> TransactionPayloadFragment.newInstance(PayloadType.REQUEST) 21 | 2 -> TransactionPayloadFragment.newInstance(PayloadType.RESPONSE) 22 | else -> throw IllegalArgumentException("no item") 23 | } 24 | 25 | override fun getCount(): Int = titles.size 26 | 27 | override fun getPageTitle(position: Int): CharSequence = titles[position] 28 | } 29 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/PostmanEchoHttpTask.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | import okhttp3.MediaType.Companion.toMediaType 4 | import okhttp3.OkHttpClient 5 | import okhttp3.Request 6 | import okhttp3.RequestBody.Companion.toRequestBody 7 | 8 | class PostmanEchoHttpTask( 9 | private val client: OkHttpClient 10 | ) : HttpTask { 11 | override fun run() { 12 | postResponsePartially() 13 | postProto() 14 | } 15 | 16 | private fun postResponsePartially() { 17 | val body = LARGE_JSON.toRequestBody("application/json".toMediaType()) 18 | val request = Request.Builder() 19 | .url("https://postman-echo.com/post") 20 | .post(body) 21 | .build() 22 | client.newCall(request).enqueue(ReadBytesCallback(SEGMENT_SIZE)) 23 | } 24 | 25 | private fun postProto() { 26 | val pokemon = Pokemon("Pikachu", level = 99) 27 | val body = pokemon.encodeByteString().toRequestBody("application/protobuf".toMediaType()) 28 | val request = Request.Builder() 29 | .url("https://postman-echo.com/post") 30 | .post(body) 31 | .build() 32 | client.newCall(request).enqueue(ReadBytesCallback()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | org.gradle.parallel=true 18 | 19 | android.useAndroidX=true 20 | 21 | VERSION_NAME=4.1.0-SNAPSHOT 22 | # 4*100*100 + 1*100 + 0 => 40100 23 | VERSION_CODE=40100 24 | GROUP=com.github.chuckerteam.chucker 25 | 26 | POM_REPO_NAME=Chucker 27 | POM_DESCRIPTION=Android in-app HTTP inspector 28 | POM_URL=https://github.com/ChuckerTeam/chucker 29 | POM_SCM_CONNECTION=scm:git:git://github.com/ChuckerTeam/chucker.git 30 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 31 | POM_LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/response/Content.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry.response 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.google.gson.annotations.SerializedName 5 | 6 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#content 7 | // http://www.softwareishard.com/blog/har-12-spec/#content 8 | internal data class Content( 9 | @SerializedName("size") val size: Long? = null, 10 | @SerializedName("compression") val compression: Int? = null, 11 | @SerializedName("mimeType") val mimeType: String, 12 | @SerializedName("text") val text: String? = null, 13 | @SerializedName("encoding") val encoding: String? = null, 14 | @SerializedName("comment") val comment: String? = null 15 | ) { 16 | 17 | companion object { 18 | internal val EMPTY = Content( 19 | size = 0L, 20 | compression = 0, 21 | mimeType = "text/plain", 22 | text = "" 23 | ) 24 | } 25 | constructor(transaction: HttpTransaction) : this( 26 | size = transaction.responsePayloadSize, 27 | mimeType = transaction.responseContentType ?: "application/octet-stream", 28 | text = transaction.responseBody 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/res/menu/chucker_transactions_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 15 | 16 | 17 | 20 | 23 | 24 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/OkioUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okio.Buffer 4 | import okio.ByteString 5 | import java.io.EOFException 6 | import kotlin.math.min 7 | 8 | private const val MAX_PREFIX_SIZE = 64L 9 | private const val CODE_POINT_SIZE = 16 10 | 11 | /** 12 | * Returns true if the [Buffer] contains human readable text. Uses a small sample 13 | * of code points to detect unicode control characters commonly used in binary file signatures. 14 | */ 15 | internal val Buffer.isProbablyPlainText 16 | get() = try { 17 | val prefix = Buffer() 18 | val byteCount = min(size, MAX_PREFIX_SIZE) 19 | copyTo(prefix, 0, byteCount) 20 | sequence { while (!prefix.exhausted()) yield(prefix.readUtf8CodePoint()) } 21 | .take(CODE_POINT_SIZE) 22 | .all { codePoint -> codePoint.isPlainTextChar() } 23 | } catch (_: EOFException) { 24 | false // Truncated UTF-8 sequence 25 | } 26 | 27 | internal val ByteString.isProbablyPlainText: Boolean 28 | get() { 29 | val byteCount = min(size, MAX_PREFIX_SIZE.toInt()) 30 | return Buffer().write(this, offset = 0, byteCount).isProbablyPlainText 31 | } 32 | 33 | private fun Int.isPlainTextChar() = Character.isWhitespace(this) || !Character.isISOControl(this) 34 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/api/BodyDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import okhttp3.Request 4 | import okhttp3.Response 5 | import okio.ByteString 6 | import okio.IOException 7 | 8 | /** 9 | * Decodes HTTP request and response bodies to human–readable texts. 10 | */ 11 | public interface BodyDecoder { 12 | /** 13 | * Returns a text representation of [body] that will be displayed in Chucker UI transaction, 14 | * or `null` if [request] cannot be handled by this decoder. [Body][body] is no longer than 15 | * [max content length][ChuckerInterceptor.Builder.maxContentLength] and is guaranteed to be 16 | * uncompressed even if [request] has gzip or br header. 17 | */ 18 | @Throws(IOException::class) 19 | public fun decodeRequest(request: Request, body: ByteString): String? 20 | 21 | /** 22 | * Returns a text representation of [body] that will be displayed in Chucker UI transaction, 23 | * or `null` if [response] cannot be handled by this decoder. [Body][body] is no longer than 24 | * [max content length][ChuckerInterceptor.Builder.maxContentLength] and is guaranteed to be 25 | * uncompressed even if [response] has gzip or br header. 26 | */ 27 | @Throws(IOException::class) 28 | public fun decodeResponse(response: Response, body: ByteString): String? 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 5 | import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple 6 | 7 | /** 8 | * Repository Interface representing all the operations that are needed to let Chucker work 9 | * with [HttpTransaction] and [HttpTransactionTuple]. Please use [HttpTransactionDatabaseRepository] that 10 | * uses Room and SqLite to run those operations. 11 | */ 12 | internal interface HttpTransactionRepository { 13 | 14 | suspend fun insertTransaction(transaction: HttpTransaction) 15 | 16 | suspend fun updateTransaction(transaction: HttpTransaction): Int 17 | 18 | suspend fun deleteOldTransactions(threshold: Long) 19 | 20 | suspend fun deleteAllTransactions() 21 | 22 | fun getSortedTransactionTuples(): LiveData> 23 | 24 | fun getFilteredTransactionTuples(code: String, path: String): LiveData> 25 | 26 | fun getTransaction(transactionId: Long): LiveData 27 | 28 | suspend fun getAllTransactions(): List 29 | 30 | fun getTransactionsInTimeRange(minTimestamp: Long?): List 31 | } 32 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/RepositoryProvider.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.repository 2 | 3 | import android.content.Context 4 | import androidx.annotation.VisibleForTesting 5 | import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider.initialize 6 | import com.chuckerteam.chucker.internal.data.room.ChuckerDatabase 7 | 8 | /** 9 | * A singleton to hold the [HttpTransactionRepository] instance. 10 | * Make sure you call [initialize] before accessing the stored instance. 11 | */ 12 | internal object RepositoryProvider { 13 | 14 | private var transactionRepository: HttpTransactionRepository? = null 15 | 16 | fun transaction(): HttpTransactionRepository { 17 | return checkNotNull(transactionRepository) { 18 | "You can't access the transaction repository if you don't initialize it!" 19 | } 20 | } 21 | 22 | /** 23 | * Idempotent method. Must be called before accessing the repositories. 24 | */ 25 | fun initialize(applicationContext: Context) { 26 | if (transactionRepository == null) { 27 | val db = ChuckerDatabase.create(applicationContext) 28 | transactionRepository = HttpTransactionDatabaseRepository(db) 29 | } 30 | } 31 | 32 | /** 33 | * Cleanup stored singleton objects 34 | */ 35 | @VisibleForTesting 36 | fun close() { 37 | transactionRepository = null 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /library-no-op/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdk rootProject.compileSdkVersion 6 | namespace "com.chuckerteam.chucker" 7 | 8 | compileOptions { 9 | kotlinOptions.freeCompilerArgs += [ 10 | '-module-name', "com.github.ChuckerTeam.Chucker.library-no-op", 11 | "-Xexplicit-api=strict" 12 | ] 13 | sourceCompatibility JavaVersion.VERSION_11 14 | targetCompatibility JavaVersion.VERSION_11 15 | } 16 | 17 | kotlin { 18 | jvmToolchain(11) 19 | } 20 | 21 | buildFeatures { 22 | buildConfig = false 23 | } 24 | 25 | defaultConfig { 26 | minSdk rootProject.minSdkVersion 27 | } 28 | 29 | lintOptions { 30 | warningsAsErrors true 31 | abortOnError true 32 | // We don't want to impose RTL on consuming applications. 33 | disable 'RtlEnabled' 34 | // Don't fail build if some dependencies outdated 35 | disable 'GradleDependency' 36 | } 37 | 38 | publishing { 39 | singleVariant('release') { 40 | withSourcesJar() 41 | withJavadocJar() 42 | } 43 | } 44 | } 45 | 46 | dependencies { 47 | api "com.squareup.okhttp3:okhttp:$okhttpVersion" 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 49 | } 50 | 51 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 52 | apply from: rootProject.file('gradle/kotlin-static-analysis.gradle') 53 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | publish: 9 | if: ${{ github.repository == 'ChuckerTeam/chucker'}} 10 | runs-on: [ubuntu-latest] 11 | 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Java 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'zulu' 20 | java-version: '17' 21 | 22 | - name: Setup Gradle 23 | uses: gradle/gradle-build-action@v2 24 | 25 | - name: Publish to Maven Local 26 | run: ./gradlew publishToMavenLocal 27 | env: 28 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} 29 | ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} 30 | 31 | - name: Upload Build Artifacts 32 | uses: actions/upload-artifact@v3 33 | with: 34 | name: 'chucker-release-artifacts' 35 | path: '~/.m2/repository/' 36 | 37 | - name: Publish to the Staging Repository 38 | run: ./gradlew publishReleasePublicationToStagingRepository --no-parallel 39 | env: 40 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} 41 | ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} 42 | ORG_GRADLE_PROJECT_NEXUS_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_USERNAME }} 43 | ORG_GRADLE_PROJECT_NEXUS_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_PASSWORD }} 44 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/RequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.PostData 4 | import com.chuckerteam.chucker.util.HarTestUtils 5 | import com.google.common.truth.Truth.assertThat 6 | import org.junit.Test 7 | 8 | internal class RequestTest { 9 | @Test 10 | fun `request is created correctly with method`() { 11 | val request = HarTestUtils.createRequest("GET") 12 | 13 | assertThat(request?.method).isEqualTo("GET") 14 | } 15 | 16 | @Test 17 | fun `request is created correctly with url`() { 18 | val request = HarTestUtils.createRequest("GET") 19 | 20 | assertThat(request?.url).isEqualTo("http://localhost:80/getUsers") 21 | } 22 | 23 | @Test 24 | fun `request is created correctly with http version`() { 25 | val request = HarTestUtils.createRequest("GET") 26 | 27 | assertThat(request?.httpVersion).isEqualTo("HTTP") 28 | } 29 | 30 | @Test 31 | fun `request is created correctly with post data`() { 32 | val request = HarTestUtils.createRequest("POST") 33 | 34 | assertThat(request?.postData) 35 | .isEqualTo(PostData(mimeType = "application/json", params = null, text = null)) 36 | } 37 | 38 | @Test 39 | fun `request is created correctly with body size`() { 40 | val request = HarTestUtils.createRequest("POST") 41 | 42 | assertThat(request?.bodySize).isEqualTo(1000) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/TransactionDetailsSharableTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import com.chuckerteam.chucker.util.TestTransactionFactory 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.robolectric.RobolectricTestRunner 10 | 11 | @RunWith(RobolectricTestRunner::class) 12 | internal class TransactionDetailsSharableTest { 13 | private val context: Context get() = ApplicationProvider.getApplicationContext() 14 | 15 | @Test 16 | fun `create sharable GET transaction content`() { 17 | val transaction = TestTransactionFactory.createTransaction("GET") 18 | val sharableTransaction = TransactionDetailsSharable(transaction, encodeUrls = false) 19 | 20 | val sharedContent = sharableTransaction.toSharableUtf8Content(context) 21 | 22 | assertThat(sharedContent).isEqualTo(TestTransactionFactory.expectedGetHttpTransaction) 23 | } 24 | 25 | @Test 26 | fun `create sharable POST transaction content`() { 27 | val transaction = TestTransactionFactory.createTransaction("POST") 28 | val sharableTransaction = TransactionDetailsSharable(transaction, encodeUrls = false) 29 | 30 | val sharedContent = sharableTransaction.toSharableUtf8Content(context) 31 | 32 | assertThat(sharedContent).isEqualTo(TestTransactionFactory.expectedHttpPostTransaction) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/BitmapUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Matrix 7 | import android.graphics.Paint 8 | import androidx.annotation.ColorInt 9 | import androidx.core.graphics.ColorUtils 10 | import androidx.palette.graphics.Palette 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.withContext 13 | 14 | private val BITMAP_PAINT = Paint(Paint.FILTER_BITMAP_FLAG) 15 | 16 | internal suspend fun Bitmap.calculateLuminance(): Double? { 17 | val color = Color.MAGENTA 18 | return withContext(Dispatchers.Default) { 19 | val alpha = replaceAlphaWithColor(color) 20 | return@withContext alpha.getLuminance(color) 21 | } 22 | } 23 | 24 | private fun Bitmap.replaceAlphaWithColor(@ColorInt color: Int): Bitmap { 25 | val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 26 | result.eraseColor(color) 27 | Canvas(result).apply { 28 | drawBitmap(this@replaceAlphaWithColor, Matrix(), BITMAP_PAINT) 29 | } 30 | return result 31 | } 32 | 33 | private fun Bitmap.getLuminance(@ColorInt alphaSubstitute: Int): Double? { 34 | val imagePalette = Palette.from(this) 35 | .clearFilters() 36 | .addFilter { rgb, _ -> (rgb != alphaSubstitute) } 37 | .generate() 38 | val dominantSwatch = imagePalette.dominantSwatch 39 | return dominantSwatch?.rgb?.let(ColorUtils::calculateLuminance) 40 | } 41 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharableTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import com.chuckerteam.chucker.util.TestTransactionFactory 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.robolectric.RobolectricTestRunner 10 | 11 | @RunWith(RobolectricTestRunner::class) 12 | internal class TransactionListDetailsSharableTest { 13 | private val context: Context get() = ApplicationProvider.getApplicationContext() 14 | 15 | @Test 16 | fun `create sharable content for multiple transactions`() { 17 | val transactions = List(10) { 18 | TestTransactionFactory.createTransaction(getRandomHttpMethod()) 19 | } 20 | val expectedSharedContent = transactions.joinToString( 21 | separator = "\n==================\n", 22 | prefix = "/* Export Start */\n", 23 | postfix = "\n/* Export End */\n" 24 | ) { TransactionDetailsSharable(it, encodeUrls = false).toSharableUtf8Content(context) } 25 | 26 | val sharedContent = TransactionListDetailsSharable( 27 | transactions, 28 | encodeUrls = false 29 | ).toSharableUtf8Content(context) 30 | assertThat(sharedContent).isEqualTo(expectedSharedContent) 31 | } 32 | 33 | private val requestMethods = listOf("GET", "POST", "PUT", "DELETE") 34 | 35 | private fun getRandomHttpMethod(): String = requestMethods.random() 36 | } 37 | -------------------------------------------------------------------------------- /library/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #81d4fa 4 | #121212 5 | #121212 6 | #121212 7 | #59CC94 8 | #cf6679 9 | #000000 10 | #000000 11 | #ffffff 12 | #ffffff 13 | #000000 14 | 15 | #e0e0e0 16 | #757575 17 | #e57373 18 | #e53935 19 | #ffb74d 20 | #64b5f6 21 | 22 | #ffe082 23 | #ef5350 24 | 25 | #75bec4 26 | #9e7162 27 | #669f50 28 | #777777 29 | #9FA162 30 | 31 | 32 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/HarTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import com.chuckerteam.chucker.R 6 | import com.chuckerteam.chucker.internal.data.har.log.Creator 7 | import com.chuckerteam.chucker.util.HarTestUtils.createSingleTransactionHar 8 | import com.google.common.truth.Truth.assertThat 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | 14 | @RunWith(RobolectricTestRunner::class) 15 | public class HarTest { 16 | private lateinit var context: Context 17 | 18 | @Before 19 | public fun setUp() { 20 | context = ApplicationProvider.getApplicationContext() 21 | } 22 | 23 | @Test 24 | public fun `har is created correctly with har version`() { 25 | val har = context.createSingleTransactionHar("GET") 26 | 27 | assertThat(har.log.version).isEqualTo("1.2") 28 | } 29 | 30 | @Test 31 | public fun `har is created correctly with creator`() { 32 | val har = context.createSingleTransactionHar("GET") 33 | val creator = Creator( 34 | context.getString(R.string.chucker_name), 35 | context.getString(R.string.chucker_version) 36 | ) 37 | 38 | assertThat(har.log.creator).isEqualTo(creator) 39 | } 40 | 41 | @Test 42 | public fun `har is created correctly with entries`() { 43 | val har = context.createSingleTransactionHar("GET") 44 | 45 | assertThat(har.log.entries).hasSize(1) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TransactionCurlCommandSharable.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.internal.data.entity.HttpHeader 5 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 6 | import okio.Buffer 7 | import okio.Source 8 | 9 | internal class TransactionCurlCommandSharable( 10 | private val transaction: HttpTransaction 11 | ) : Sharable { 12 | override fun toSharableContent(context: Context): Source = Buffer().apply { 13 | var compressed = false 14 | writeUtf8("curl -X ${transaction.method}") 15 | val headers = transaction.getParsedRequestHeaders() 16 | 17 | headers?.forEach { header -> 18 | if (isCompressed(header)) { 19 | compressed = true 20 | } 21 | writeUtf8(" -H \"${header.name}: ${header.value}\"") 22 | } 23 | 24 | val requestBody = transaction.requestBody 25 | if (!requestBody.isNullOrEmpty()) { 26 | // try to keep to a single line and use a subshell to preserve any line breaks 27 | writeUtf8(" --data $'${requestBody.replace("\n", "\\n")}'") 28 | } 29 | writeUtf8((if (compressed) " --compressed " else " ") + transaction.getFormattedUrl(encode = false)) 30 | } 31 | 32 | private fun isCompressed(header: HttpHeader): Boolean { 33 | return ( 34 | "Accept-Encoding".equals(header.name, ignoreCase = true) && 35 | "gzip".contains(header.value, ignoreCase = true) || 36 | "br".contains(header.value, ignoreCase = true) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | publish: 9 | if: ${{ github.repository == 'ChuckerTeam/chucker'}} 10 | runs-on: [ubuntu-latest] 11 | 12 | steps: 13 | - name: Cancel Previous Runs 14 | uses: styfle/cancel-workflow-action@0.12.0 15 | with: 16 | access_token: ${{ github.token }} 17 | 18 | - name: Checkout Repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Java 22 | uses: actions/setup-java@v3 23 | with: 24 | distribution: 'zulu' 25 | java-version: '17' 26 | 27 | - name: Setup Gradle 28 | uses: gradle/gradle-build-action@v2 29 | 30 | - name: Publish to Maven Local 31 | run: ./gradlew publishToMavenLocal 32 | env: 33 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} 34 | ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} 35 | 36 | - name: Upload Build Artifacts 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: 'chucker-snapshot-artifacts' 40 | path: '~/.m2/repository/' 41 | 42 | - name: Publish to the Snapshot Repository 43 | run: ./gradlew publishReleasePublicationToSnapshotRepository --no-parallel 44 | env: 45 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} 46 | ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} 47 | ORG_GRADLE_PROJECT_NEXUS_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_USERNAME }} 48 | ORG_GRADLE_PROJECT_NEXUS_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_NEXUS_PASSWORD }} 49 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/ResponseTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.entry.response.Content 4 | import com.chuckerteam.chucker.util.HarTestUtils 5 | import com.google.common.truth.Truth.assertThat 6 | import org.junit.Test 7 | 8 | internal class ResponseTest { 9 | @Test 10 | fun `response is created correctly with status`() { 11 | val response = HarTestUtils.createResponse("GET") 12 | 13 | assertThat(response?.status).isEqualTo(200) 14 | } 15 | 16 | @Test 17 | fun `response is created correctly with status text`() { 18 | val response = HarTestUtils.createResponse("GET") 19 | 20 | assertThat(response?.statusText).isEqualTo("OK") 21 | } 22 | 23 | @Test 24 | fun `response is created correctly with http version`() { 25 | val response = HarTestUtils.createResponse("GET") 26 | 27 | assertThat(response?.httpVersion).isEqualTo("HTTP") 28 | } 29 | 30 | @Test 31 | fun `response is created correctly with content`() { 32 | val response = HarTestUtils.createResponse("GET") 33 | 34 | assertThat(response?.content).isEqualTo( 35 | Content( 36 | size = 1000, 37 | compression = null, 38 | mimeType = "application/json", 39 | text = """{"field": "value"}""", 40 | encoding = null 41 | ) 42 | ) 43 | } 44 | 45 | @Test 46 | fun `response is created correctly with body size`() { 47 | val response = HarTestUtils.createResponse("GET") 48 | 49 | assertThat(response?.bodySize).isEqualTo(1000) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import android.content.Context 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | import java.io.IOException 7 | 8 | /** 9 | * No-op implementation. 10 | */ 11 | @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER") 12 | public class ChuckerInterceptor private constructor( 13 | builder: Builder 14 | ) : Interceptor { 15 | 16 | /** 17 | * No-op implementation. 18 | */ 19 | public constructor(context: Context) : this(Builder(context)) 20 | 21 | public fun redactHeaders(vararg names: String): ChuckerInterceptor { 22 | return this 23 | } 24 | 25 | @Throws(IOException::class) 26 | override fun intercept(chain: Interceptor.Chain): Response { 27 | val request = chain.request() 28 | return chain.proceed(request) 29 | } 30 | 31 | /** 32 | * No-op implementation. 33 | */ 34 | public class Builder(private val context: Context) { 35 | public fun collector(collector: ChuckerCollector): Builder = this 36 | 37 | public fun maxContentLength(length: Long): Builder = this 38 | 39 | public fun redactHeaders(headerNames: Iterable): Builder = this 40 | 41 | public fun redactHeaders(vararg headerNames: String): Builder = this 42 | 43 | public fun alwaysReadResponseBody(enable: Boolean): Builder = this 44 | 45 | public fun addBodyDecoder(decoder: Any): Builder = this 46 | 47 | public fun createShortcut(enable: Boolean): Builder = this 48 | 49 | public fun skipPaths(vararg paths: String): Builder = this 50 | 51 | public fun build(): ChuckerInterceptor = ChuckerInterceptor(this) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/OkioUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import okio.Buffer 5 | import okio.ByteString.Companion.encodeUtf8 6 | import org.junit.jupiter.api.Test 7 | import java.nio.charset.Charset 8 | 9 | internal class OkioUtilsTest { 10 | @Test 11 | fun `no content is probably plain text`() { 12 | val buffer = Buffer() 13 | 14 | assertThat(buffer.isProbablyPlainText).isTrue() 15 | } 16 | 17 | @Test 18 | fun `blank content is probably plain text`() { 19 | val buffer = Buffer() 20 | buffer.writeString(" ", Charset.defaultCharset()) 21 | 22 | assertThat(buffer.isProbablyPlainText).isTrue() 23 | } 24 | 25 | @Test 26 | fun `plain text content is probably plain text`() { 27 | val buffer = Buffer() 28 | buffer.writeString("just a string", Charset.defaultCharset()) 29 | 30 | assertThat(buffer.isProbablyPlainText).isTrue() 31 | } 32 | 33 | @Test 34 | fun `non plain text content is probably not plain text`() { 35 | val buffer = Buffer() 36 | buffer.writeByte(0x11000000) 37 | 38 | assertThat(buffer.isProbablyPlainText).isFalse() 39 | } 40 | 41 | @Test 42 | fun `non-ASCII content is probably plain text`() { 43 | val buffer = Buffer() 44 | buffer.writeString("ą", Charset.defaultCharset()) 45 | 46 | assertThat(buffer.isProbablyPlainText).isTrue() 47 | } 48 | 49 | @Test 50 | fun `truncated UTF-8 content is probably not plain text`() { 51 | val bytes = "ą".encodeUtf8().let { it.substring(0, it.size - 1) } 52 | val buffer = Buffer().write(bytes) 53 | 54 | assertThat(buffer.isProbablyPlainText).isFalse() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.ui 2 | 3 | import android.text.TextUtils 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.switchMap 8 | import androidx.lifecycle.viewModelScope 9 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 10 | import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple 11 | import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider 12 | import com.chuckerteam.chucker.internal.support.NotificationHelper 13 | import kotlinx.coroutines.launch 14 | 15 | internal class MainViewModel : ViewModel() { 16 | 17 | private val currentFilter = MutableLiveData("") 18 | 19 | val transactions: LiveData> = currentFilter.switchMap { searchQuery -> 20 | with(RepositoryProvider.transaction()) { 21 | when { 22 | searchQuery.isNullOrBlank() -> { 23 | getSortedTransactionTuples() 24 | } 25 | TextUtils.isDigitsOnly(searchQuery) -> { 26 | getFilteredTransactionTuples(searchQuery, "") 27 | } 28 | else -> { 29 | getFilteredTransactionTuples("", searchQuery) 30 | } 31 | } 32 | } 33 | } 34 | 35 | suspend fun getAllTransactions(): List = RepositoryProvider.transaction().getAllTransactions() 36 | 37 | fun updateItemsFilter(searchQuery: String) { 38 | currentFilter.value = searchQuery 39 | } 40 | 41 | fun clearTransactions() { 42 | viewModelScope.launch { 43 | RepositoryProvider.transaction().deleteAllTransactions() 44 | } 45 | NotificationHelper.clearBuffer() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/repository/RepositoryProviderTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.repository 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.test.core.app.ApplicationProvider 6 | import com.chuckerteam.chucker.internal.data.room.ChuckerDatabase 7 | import com.google.common.truth.Truth.assertThat 8 | import org.junit.After 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.jupiter.api.assertThrows 12 | import org.junit.runner.RunWith 13 | import org.robolectric.RobolectricTestRunner 14 | 15 | @RunWith(RobolectricTestRunner::class) 16 | internal class RepositoryProviderTest { 17 | private lateinit var db: ChuckerDatabase 18 | private lateinit var context: Context 19 | 20 | @Before 21 | fun setUp() { 22 | context = ApplicationProvider.getApplicationContext() 23 | db = Room.inMemoryDatabaseBuilder(context, ChuckerDatabase::class.java) 24 | .allowMainThreadQueries() 25 | .build() 26 | } 27 | 28 | @After 29 | fun tearDown() { 30 | db.close() 31 | RepositoryProvider.close() 32 | } 33 | 34 | @Test 35 | fun `fails with uninitialized transaction repository`() { 36 | assertThrows { 37 | RepositoryProvider.transaction() 38 | } 39 | } 40 | 41 | @Test 42 | fun `transaction repository is available after initialization`() { 43 | RepositoryProvider.initialize(context) 44 | assertThat(RepositoryProvider.transaction()).isNotNull() 45 | } 46 | 47 | @Test 48 | fun `transaction repository is cached`() { 49 | RepositoryProvider.initialize(context) 50 | val one = RepositoryProvider.transaction() 51 | val two = RepositoryProvider.transaction() 52 | assertThat(one).isSameInstanceAs(two) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TeeSource.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okio.Buffer 4 | import okio.Sink 5 | import okio.Source 6 | import okio.Timeout 7 | import java.io.IOException 8 | 9 | /** 10 | * A source that acts as a tee operator - https://en.wikipedia.org/wiki/Tee_(command). 11 | * 12 | * It takes the input [upstream] and reads from it serving the bytes to the end consumer 13 | * like a regular [Source]. While bytes are read from the [upstream] the are also copied 14 | * to a [sideStream]. 15 | */ 16 | internal class TeeSource( 17 | private val upstream: Source, 18 | private val sideStream: Sink 19 | ) : Source { 20 | private val tempBuffer = Buffer() 21 | private var isFailure = false 22 | 23 | override fun read(sink: Buffer, byteCount: Long): Long { 24 | val bytesRead = upstream.read(sink, byteCount) 25 | 26 | if (bytesRead == -1L) { 27 | safeCloseSideStream() 28 | return -1L 29 | } 30 | 31 | if (!isFailure) { 32 | copyBytesToSideStream(sink, bytesRead) 33 | } 34 | 35 | return bytesRead 36 | } 37 | 38 | private fun copyBytesToSideStream(sink: Buffer, bytesRead: Long) { 39 | val offset = sink.size - bytesRead 40 | sink.copyTo(tempBuffer, offset, bytesRead) 41 | try { 42 | sideStream.write(tempBuffer, bytesRead) 43 | } catch (_: IOException) { 44 | isFailure = true 45 | safeCloseSideStream() 46 | } 47 | } 48 | 49 | override fun close() { 50 | safeCloseSideStream() 51 | upstream.close() 52 | } 53 | 54 | private fun safeCloseSideStream() = try { 55 | sideStream.close() 56 | } catch (_: IOException) { 57 | isFailure = true 58 | } 59 | 60 | override fun timeout(): Timeout = upstream.timeout() 61 | } 62 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Response.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.log.entry.response.Content 5 | import com.chuckerteam.chucker.internal.data.har.log.entry.response.Content.Companion.EMPTY 6 | import com.google.gson.annotations.SerializedName 7 | 8 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#response 9 | // http://www.softwareishard.com/blog/har-12-spec/#response 10 | internal data class Response( 11 | @SerializedName("status") val status: Int, 12 | @SerializedName("statusText") val statusText: String, 13 | @SerializedName("httpVersion") val httpVersion: String, 14 | @SerializedName("cookies") val cookies: List = emptyList(), 15 | @SerializedName("headers") val headers: List
, 16 | @SerializedName("content") val content: Content? = null, 17 | @SerializedName("redirectURL") val redirectUrl: String = "", 18 | @SerializedName("headersSize") val headersSize: Long, 19 | @SerializedName("bodySize") val bodySize: Long, 20 | @SerializedName("totalSize") val totalSize: Long, 21 | @SerializedName("comment") val comment: String? = null 22 | ) { 23 | constructor(transaction: HttpTransaction) : this( 24 | status = transaction.responseCode ?: 0, 25 | statusText = transaction.responseMessage ?: "", 26 | httpVersion = transaction.protocol ?: "", 27 | headers = transaction.getParsedResponseHeaders()?.map { Header(it) } ?: emptyList(), 28 | content = transaction.responsePayloadSize?.run { Content(transaction) } ?: EMPTY, 29 | headersSize = transaction.responseHeadersSize ?: 0, 30 | bodySize = transaction.getHarResponseBodySize(), 31 | totalSize = transaction.getResponseTotalSize() 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chucker_ic_graphql.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #01579b 4 | #002f6c 5 | #ffffff 6 | #ffffff 7 | #009E09 8 | #F44336 9 | #ffffff 10 | #000000 11 | #000000 12 | #000000 13 | #ffffff 14 | 15 | #212121 16 | #9E9E9E 17 | #F44336 18 | #B71C1C 19 | #FF9800 20 | #0D47A1 21 | 22 | #ffffff00 23 | #ffff0000 24 | 25 | #F8FAFC 26 | #D2DADF 27 | #182531 28 | #01101D 29 | 30 | #8B0057 31 | #2F00FF 32 | #E84B31 33 | #474747 34 | #2F99AA 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /library/src/main/res/menu/chucker_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 17 | 18 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Request.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log.entry 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.PostData 5 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.QueryString 6 | import com.google.gson.annotations.SerializedName 7 | import okhttp3.HttpUrl.Companion.toHttpUrl 8 | 9 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#request 10 | // http://www.softwareishard.com/blog/har-12-spec/#request 11 | internal data class Request( 12 | @SerializedName("method") val method: String, 13 | @SerializedName("url") val url: String, 14 | @SerializedName("httpVersion") val httpVersion: String, 15 | @SerializedName("cookies") val cookies: List = emptyList(), 16 | @SerializedName("headers") val headers: List
, 17 | @SerializedName("queryString") val queryString: List, 18 | @SerializedName("postData") val postData: PostData? = null, 19 | @SerializedName("headersSize") val headersSize: Long, 20 | @SerializedName("bodySize") val bodySize: Long, 21 | @SerializedName("totalSize") val totalSize: Long, 22 | @SerializedName("comment") val comment: String? = null 23 | ) { 24 | constructor(transaction: HttpTransaction) : this( 25 | method = transaction.method ?: "", 26 | url = transaction.url ?: "", 27 | httpVersion = transaction.protocol ?: "", 28 | headers = transaction.getParsedRequestHeaders()?.map { Header(it) } ?: emptyList(), 29 | queryString = transaction.url?.let { QueryString.fromUrl(it.toHttpUrl()) } ?: emptyList(), 30 | postData = transaction.requestPayloadSize?.run { PostData(transaction) }, 31 | headersSize = transaction.requestHeadersSize ?: 0, 32 | bodySize = transaction.requestPayloadSize ?: 0, 33 | totalSize = transaction.getRequestTotalSize() 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 29 | 30 | 34 | 35 | 38 | 39 | 44 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chucker_activity_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 25 | 26 | 29 | 30 | 34 | 35 | 40 | 41 | 44 | 45 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import com.chuckerteam.chucker.util.test 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.Rule 8 | import org.junit.Test 9 | 10 | internal class LiveDataCombineLatestTest { 11 | @get:Rule val instantExecutorRule = InstantTaskExecutorRule() 12 | 13 | private val inputA = MutableLiveData() 14 | private val inputB = MutableLiveData() 15 | 16 | private val upstream = inputA.combineLatest(inputB) 17 | 18 | @Test 19 | fun `downstream does not emit if the first source has no values`() { 20 | upstream.test { 21 | inputB.value = 1 22 | inputB.value = 2 23 | inputB.value = 3 24 | 25 | expectNoData() 26 | } 27 | } 28 | 29 | @Test 30 | fun `downstream does not emit if the second source has no values`() { 31 | upstream.test { 32 | inputA.value = true 33 | inputA.value = false 34 | 35 | expectNoData() 36 | } 37 | } 38 | 39 | @Test 40 | fun `downstream combines source values`() { 41 | upstream.test { 42 | inputA.value = true 43 | inputB.value = 1 44 | 45 | assertThat(expectData()).isEqualTo(true to 1) 46 | } 47 | } 48 | 49 | @Test 50 | fun `downstream updates with updates to the second source`() { 51 | upstream.test { 52 | inputA.value = true 53 | inputB.value = 1 54 | assertThat(expectData()).isEqualTo(true to 1) 55 | 56 | inputB.value = 2 57 | assertThat(expectData()).isEqualTo(true to 2) 58 | } 59 | } 60 | 61 | @Test 62 | fun `downstream updates with updates to the first source`() { 63 | upstream.test { 64 | inputA.value = true 65 | inputB.value = 1 66 | assertThat(expectData()).isEqualTo(true to 1) 67 | 68 | inputA.value = false 69 | assertThat(expectData()).isEqualTo(false to 1) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/util/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.util 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | import com.chuckerteam.chucker.internal.support.hasBody 6 | import okhttp3.HttpUrl 7 | import okhttp3.Request 8 | import okhttp3.RequestBody 9 | import okhttp3.Response 10 | import okio.Buffer 11 | import okio.ByteString 12 | import okio.buffer 13 | import okio.source 14 | import java.io.File 15 | 16 | internal const val SEGMENT_SIZE = 8_192L 17 | 18 | internal fun getResourceFile(file: String): Buffer { 19 | return Buffer().apply { 20 | writeAll(File("./src/test/resources/$file").source().buffer()) 21 | } 22 | } 23 | 24 | internal fun Response.readByteStringBody(length: Long? = null): ByteString? { 25 | return if (hasBody()) { 26 | body?.source()?.use { source -> 27 | if (length == null) { 28 | source.readByteString() 29 | } else { 30 | source.readByteString(length) 31 | } 32 | } 33 | } else { 34 | null 35 | } 36 | } 37 | 38 | internal fun RequestBody.toServerRequest(serverUrl: HttpUrl) = Request.Builder().url(serverUrl).post(this).build() 39 | 40 | internal fun LiveData.test(test: LiveDataRecord.() -> Unit) { 41 | val observer = RecordingObserver() 42 | observeForever(observer) 43 | LiveDataRecord(observer).test() 44 | removeObserver(observer) 45 | observer.records.clear() 46 | } 47 | 48 | internal class LiveDataRecord internal constructor( 49 | private val observer: RecordingObserver 50 | ) { 51 | fun expectData(): T { 52 | if (observer.records.isEmpty()) { 53 | throw AssertionError("Expected data but was empty.") 54 | } 55 | return observer.records.removeAt(0) 56 | } 57 | 58 | fun expectNoData() { 59 | if (observer.records.isNotEmpty()) { 60 | val data = observer.records[0] 61 | throw AssertionError("Expected no data but was $data.") 62 | } 63 | } 64 | } 65 | 66 | internal class RecordingObserver : Observer { 67 | val records = mutableListOf() 68 | 69 | override fun onChanged(data: T) { 70 | records += data 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/Entry.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har.log 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 5 | import com.chuckerteam.chucker.internal.data.har.log.entry.Cache 6 | import com.chuckerteam.chucker.internal.data.har.log.entry.Request 7 | import com.chuckerteam.chucker.internal.data.har.log.entry.Response 8 | import com.chuckerteam.chucker.internal.data.har.log.entry.Timings 9 | import com.google.gson.annotations.SerializedName 10 | import java.text.SimpleDateFormat 11 | import java.util.Date 12 | import java.util.Locale 13 | 14 | // https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#entries 15 | // http://www.softwareishard.com/blog/har-12-spec/#entries 16 | internal data class Entry( 17 | @SerializedName("pageref") val pageref: String? = null, 18 | @SerializedName("startedDateTime") val startedDateTime: String, 19 | @SerializedName("time") var time: Long, 20 | @SerializedName("request") val request: Request, 21 | @SerializedName("response") val response: Response, 22 | @SerializedName("cache") val cache: Cache, 23 | @SerializedName("timings") val timings: Timings, 24 | @SerializedName("serverIPAddress") val serverIPAddress: String? = null, 25 | @SerializedName("connection") val connection: String? = null, 26 | @SerializedName("comment") val comment: String? = null 27 | ) { 28 | constructor(transaction: HttpTransaction) : this( 29 | startedDateTime = transaction.requestDate?.harFormatted().orEmpty(), 30 | time = Timings(transaction).getTime(), 31 | request = Request(transaction), 32 | response = Response(transaction), 33 | cache = Cache(), 34 | timings = Timings(transaction) 35 | ) 36 | 37 | @VisibleForTesting 38 | object DateFormat : ThreadLocal() { 39 | override fun initialValue(): SimpleDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US) 40 | } 41 | 42 | companion object { 43 | private const val DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 44 | 45 | private fun Long.harFormatted(): String { 46 | return DateFormat.get()!!.format(Date(this)) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/FormattedUrl.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okhttp3.HttpUrl 4 | 5 | internal class FormattedUrl private constructor( 6 | val scheme: String, 7 | val host: String, 8 | val port: Int, 9 | val path: String, 10 | val query: String 11 | ) { 12 | val pathWithQuery: String 13 | get() = if (query.isBlank()) { 14 | path 15 | } else { 16 | "$path?$query" 17 | } 18 | 19 | val url: String 20 | get() { 21 | return if (shouldShowPort()) { 22 | "$scheme://$host:$port$pathWithQuery" 23 | } else { 24 | "$scheme://$host$pathWithQuery" 25 | } 26 | } 27 | 28 | private fun shouldShowPort(): Boolean { 29 | if (scheme == "https" && port == HTTPS_PORT) { 30 | return false 31 | } 32 | if (scheme == "http" && port == HTTP_PORT) { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | companion object { 39 | private const val HTTPS_PORT = 443 40 | private const val HTTP_PORT = 80 41 | 42 | fun fromHttpUrl(httpUrl: HttpUrl, encoded: Boolean): FormattedUrl { 43 | return if (encoded) { 44 | encodedUrl(httpUrl) 45 | } else { 46 | decodedUrl(httpUrl) 47 | } 48 | } 49 | 50 | private fun encodedUrl(httpUrl: HttpUrl): FormattedUrl { 51 | val path = httpUrl.encodedPathSegments.joinToString("/") 52 | return FormattedUrl( 53 | httpUrl.scheme, 54 | httpUrl.host, 55 | httpUrl.port, 56 | if (path.isNotBlank()) "/$path" else "", 57 | httpUrl.encodedQuery.orEmpty() 58 | ) 59 | } 60 | 61 | private fun decodedUrl(httpUrl: HttpUrl): FormattedUrl { 62 | val path = httpUrl.pathSegments.joinToString("/") 63 | return FormattedUrl( 64 | httpUrl.scheme, 65 | httpUrl.host, 66 | httpUrl.port, 67 | if (path.isNotBlank()) "/$path" else "", 68 | httpUrl.query.orEmpty() 69 | ) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chuckerteam/chucker/sample/GraphQlTask.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.sample 2 | 3 | import com.apollographql.apollo3.ApolloClient 4 | import com.apollographql.apollo3.api.Optional 5 | import com.apollographql.apollo3.network.okHttpClient 6 | import kotlinx.coroutines.MainScope 7 | import kotlinx.coroutines.launch 8 | import okhttp3.OkHttpClient 9 | import okhttp3.ResponseBody 10 | import retrofit2.Call 11 | import retrofit2.Callback 12 | import retrofit2.Response 13 | import retrofit2.Retrofit 14 | import retrofit2.create 15 | import retrofit2.http.GET 16 | import retrofit2.http.Query 17 | 18 | private const val GRAPHQL_BASE_URL = "https://rickandmortyapi.com/graphql/" 19 | private const val BASE_URL = "https://rickandmortyapi.com/" 20 | class GraphQlTask( 21 | client: OkHttpClient 22 | ) : HttpTask { 23 | 24 | private val apolloClient = ApolloClient.Builder() 25 | .serverUrl(GRAPHQL_BASE_URL) 26 | .okHttpClient(client) 27 | .build() 28 | 29 | private val api = Retrofit.Builder() 30 | .baseUrl(BASE_URL) 31 | .client(client) 32 | .build() 33 | .create() 34 | 35 | private val scope = MainScope() 36 | 37 | override fun run() { 38 | scope.launch { 39 | api.getCharacterById(GRAPHQL_QUERY, GRAPHQL_QUERY_VARIABLE).enqueue(object : Callback { 40 | override fun onResponse(call: Call, response: Response) = Unit 41 | 42 | override fun onFailure(call: Call, t: Throwable) { 43 | t.printStackTrace() 44 | } 45 | }) 46 | apolloClient 47 | .query(SearchCharactersQuery(Optional.presentIfNotNull("Morty"))) 48 | .execute() 49 | } 50 | } 51 | 52 | private interface Api { 53 | @GET("graphql") 54 | fun getCharacterById( 55 | @Query("query") query: String, 56 | @Query("variables") variables: String? = null 57 | ): Call 58 | } 59 | } 60 | 61 | const val GRAPHQL_QUERY = """query GetCharacter( ${'$'}id: ID! ){ 62 | character(id:${'$'}id) { 63 | id:id, 64 | name, 65 | status 66 | } 67 | }""" 68 | const val GRAPHQL_QUERY_VARIABLE = """{"id":1}""" 69 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.room 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.Dao 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Update 9 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 10 | import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple 11 | 12 | @Dao 13 | internal interface HttpTransactionDao { 14 | 15 | @Query( 16 | "SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " + 17 | "requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " + 18 | "transactions ORDER BY requestDate DESC" 19 | ) 20 | fun getSortedTuples(): LiveData> 21 | 22 | @Query( 23 | "SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " + 24 | "requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " + 25 | "transactions WHERE responseCode LIKE :codeQuery AND (path LIKE :pathQuery OR " + 26 | "graphQlOperationName LIKE :graphQlQuery) ORDER BY requestDate DESC" 27 | ) 28 | fun getFilteredTuples( 29 | codeQuery: String, 30 | pathQuery: String, 31 | graphQlQuery: String = "" 32 | ): LiveData> 33 | 34 | @Insert 35 | suspend fun insert(transaction: HttpTransaction): Long? 36 | 37 | @Update(onConflict = OnConflictStrategy.REPLACE) 38 | suspend fun update(transaction: HttpTransaction): Int 39 | 40 | @Query("DELETE FROM transactions") 41 | suspend fun deleteAll(): Int 42 | 43 | @Query("SELECT * FROM transactions WHERE id = :id") 44 | fun getById(id: Long): LiveData 45 | 46 | @Query("DELETE FROM transactions WHERE requestDate <= :threshold") 47 | suspend fun deleteBefore(threshold: Long): Int 48 | 49 | @Query("SELECT * FROM transactions") 50 | suspend fun getAll(): List 51 | 52 | @Query("SELECT * FROM transactions WHERE requestDate >= :timestamp") 53 | fun getTransactionsInTimeRange(timestamp: Long): List 54 | } 55 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/LiveDataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.arch.core.executor.ArchTaskExecutor 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MediatorLiveData 7 | import java.util.concurrent.Executor 8 | 9 | internal fun LiveData.combineLatest( 10 | other: LiveData, 11 | func: (T1, T2) -> R 12 | ): LiveData { 13 | return MediatorLiveData().apply { 14 | var lastA: T1? = null 15 | var lastB: T2? = null 16 | 17 | addSource(this@combineLatest) { 18 | lastA = it 19 | val observedB = lastB 20 | if (it == null && value != null) { 21 | value = null 22 | } else if (it != null && observedB != null) { 23 | value = func(it, observedB) 24 | } 25 | } 26 | 27 | addSource(other) { 28 | lastB = it 29 | val observedA = lastA 30 | if (it == null && value != null) { 31 | value = null 32 | } else if (observedA != null && it != null) { 33 | value = func(observedA, it) 34 | } 35 | } 36 | } 37 | } 38 | 39 | internal fun LiveData.combineLatest(other: LiveData): LiveData> { 40 | return combineLatest(other) { a, b -> a to b } 41 | } 42 | 43 | // Unlike built-in extension operation is performed on a provided thread pool. 44 | // This is needed in our case since we compare requests and responses which can be big 45 | // and result in frame drops. 46 | internal fun LiveData.distinctUntilChanged( 47 | executor: Executor = ioExecutor(), 48 | areEqual: (old: T, new: T) -> Boolean = { old, new -> old == new } 49 | ): LiveData { 50 | val distinctMediator = MediatorLiveData() 51 | var old = uninitializedToken 52 | distinctMediator.addSource(this) { new -> 53 | executor.execute { 54 | @Suppress("UNCHECKED_CAST") 55 | if (old === uninitializedToken || !areEqual(old as T, new)) { 56 | old = new 57 | distinctMediator.postValue(new) 58 | } 59 | } 60 | } 61 | return distinctMediator 62 | } 63 | 64 | private val uninitializedToken: Any? = Any() 65 | 66 | // It is lesser evil than providing a custom executor. 67 | @SuppressLint("RestrictedApi") 68 | private fun ioExecutor() = ArchTaskExecutor.getIOThreadExecutor() 69 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 5 | import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple 6 | import com.chuckerteam.chucker.internal.data.room.ChuckerDatabase 7 | import com.chuckerteam.chucker.internal.support.distinctUntilChanged 8 | 9 | internal class HttpTransactionDatabaseRepository(private val database: ChuckerDatabase) : HttpTransactionRepository { 10 | 11 | private val transactionDao get() = database.transactionDao() 12 | 13 | override fun getFilteredTransactionTuples(code: String, path: String): LiveData> { 14 | val pathQuery = if (path.isNotEmpty()) "%$path%" else "%" 15 | return transactionDao.getFilteredTuples( 16 | "$code%", 17 | pathQuery = pathQuery, 18 | /** 19 | * Refer (false) 15 | 16 | val encodeUrl: LiveData = mutableEncodeUrl 17 | 18 | val transactionTitle: LiveData = RepositoryProvider.transaction() 19 | .getTransaction(transactionId) 20 | .combineLatest(encodeUrl) { transaction, encodeUrl -> 21 | if (transaction != null) "${transaction.method} ${transaction.getFormattedPath(encode = encodeUrl)}" else "" 22 | } 23 | 24 | val doesUrlRequireEncoding: LiveData = RepositoryProvider.transaction() 25 | .getTransaction(transactionId) 26 | .map { transaction -> 27 | if (transaction == null) { 28 | false 29 | } else { 30 | transaction.getFormattedPath(encode = true) != transaction.getFormattedPath(encode = false) 31 | } 32 | } 33 | 34 | val doesRequestBodyRequireEncoding: LiveData = RepositoryProvider.transaction() 35 | .getTransaction(transactionId) 36 | .map { transaction -> 37 | transaction?.requestContentType?.contains("x-www-form-urlencoded", ignoreCase = true) ?: false 38 | } 39 | 40 | val transaction: LiveData = RepositoryProvider.transaction().getTransaction(transactionId) 41 | 42 | val formatRequestBody: LiveData = doesRequestBodyRequireEncoding 43 | .combineLatest(encodeUrl) { requiresEncoding, encodeUrl -> 44 | !(requiresEncoding && encodeUrl) 45 | } 46 | 47 | fun switchUrlEncoding() = encodeUrl(!encodeUrl.value!!) 48 | 49 | fun encodeUrl(encode: Boolean) { 50 | mutableEncodeUrl.value = encode 51 | } 52 | } 53 | 54 | internal class TransactionViewModelFactory( 55 | private val transactionId: Long = 0L 56 | ) : ViewModelProvider.NewInstanceFactory() { 57 | override fun create(modelClass: Class): T { 58 | require(modelClass == TransactionViewModel::class.java) { "Cannot create $modelClass" } 59 | @Suppress("UNCHECKED_CAST") 60 | return TransactionViewModel(transactionId) as T 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransactionTuple.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import com.chuckerteam.chucker.internal.support.FormatUtils 5 | import com.chuckerteam.chucker.internal.support.FormattedUrl 6 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 7 | 8 | /** 9 | * A subset of [HttpTransaction] to perform faster Read operations on the Repository. 10 | * This Tuple is good to be used on List or Preview interfaces. 11 | */ 12 | @Suppress("LongParameterList") 13 | internal data class HttpTransactionTuple( 14 | @ColumnInfo(name = "id") var id: Long, 15 | @ColumnInfo(name = "requestDate") var requestDate: Long?, 16 | @ColumnInfo(name = "tookMs") var tookMs: Long?, 17 | @ColumnInfo(name = "protocol") var protocol: String?, 18 | @ColumnInfo(name = "method") var method: String?, 19 | @ColumnInfo(name = "host") var host: String?, 20 | @ColumnInfo(name = "path") var path: String?, 21 | @ColumnInfo(name = "scheme") var scheme: String?, 22 | @ColumnInfo(name = "responseCode") var responseCode: Int?, 23 | @ColumnInfo(name = "requestPayloadSize") var requestPayloadSize: Long?, 24 | @ColumnInfo(name = "responsePayloadSize") var responsePayloadSize: Long?, 25 | @ColumnInfo(name = "error") var error: String?, 26 | @ColumnInfo(name = "graphQlDetected") var graphQlDetected: Boolean = false, 27 | @ColumnInfo(name = "graphQlOperationName") var graphQlOperationName: String? 28 | ) { 29 | val isSsl: Boolean get() = scheme.equals("https", ignoreCase = true) 30 | 31 | val status: HttpTransaction.Status 32 | get() = when { 33 | error != null -> HttpTransaction.Status.Failed 34 | responseCode == null -> HttpTransaction.Status.Requested 35 | else -> HttpTransaction.Status.Complete 36 | } 37 | 38 | val durationString: String? get() = tookMs?.let { "$it ms" } 39 | 40 | val totalSizeString: String 41 | get() { 42 | val reqBytes = requestPayloadSize ?: 0 43 | val resBytes = responsePayloadSize ?: 0 44 | return formatBytes(reqBytes + resBytes) 45 | } 46 | 47 | private fun formatBytes(bytes: Long): String { 48 | return FormatUtils.formatByteCount(bytes, true) 49 | } 50 | fun getFormattedPath(encode: Boolean): String { 51 | val path = this.path ?: return "" 52 | 53 | // Create dummy URL since there is no data in this class to get it from 54 | // and we are only interested in a formatted path with query. 55 | val dummyUrl = "https://www.example.com$path" 56 | 57 | val httpUrl = dummyUrl.toHttpUrlOrNull() ?: return "" 58 | return FormattedUrl.fromHttpUrl(httpUrl, encode).pathWithQuery 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/Sharable.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.app.Activity 4 | import android.content.ClipData 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import androidx.core.app.ShareCompat 9 | import androidx.core.content.FileProvider 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.withContext 12 | import okio.BufferedSource 13 | import okio.Source 14 | import okio.buffer 15 | import okio.sink 16 | 17 | internal interface Sharable { 18 | fun toSharableContent(context: Context): Source 19 | } 20 | 21 | internal fun Sharable.toSharableUtf8Content( 22 | context: Context 23 | ) = toSharableContent(context).buffer().use(BufferedSource::readUtf8) 24 | 25 | internal suspend fun Sharable.shareAsUtf8Text( 26 | activity: Activity, 27 | intentTitle: String, 28 | intentSubject: String 29 | ): Intent { 30 | val content = withContext(Dispatchers.Default) { toSharableUtf8Content(activity) } 31 | return ShareCompat.IntentBuilder(activity) 32 | .setType("text/plain") 33 | .setChooserTitle(intentTitle) 34 | .setSubject(intentSubject) 35 | .setText(content) 36 | .createChooserIntent() 37 | } 38 | 39 | internal fun Sharable.writeToFile( 40 | context: Context, 41 | fileName: String 42 | ): Uri? { 43 | val cache = context.cacheDir 44 | if (cache == null) { 45 | Logger.warn("Failed to obtain a valid cache directory for file export") 46 | return null 47 | } 48 | 49 | val file = FileFactory.create(cache, fileName) 50 | if (file == null) { 51 | Logger.warn("Failed to create an export file") 52 | return null 53 | } 54 | 55 | val fileContent = toSharableContent(context) 56 | file.sink().buffer().use { it.writeAll(fileContent) } 57 | 58 | return FileProvider.getUriForFile( 59 | context, 60 | "${context.packageName}.com.chuckerteam.chucker.provider", 61 | file 62 | ) 63 | } 64 | 65 | internal fun Sharable.shareAsFile( 66 | activity: Activity, 67 | fileName: String, 68 | intentTitle: String, 69 | intentSubject: String, 70 | clipDataLabel: String 71 | ): Intent? { 72 | val uri = writeToFile(activity, fileName) ?: return null 73 | val shareIntent = ShareCompat.IntentBuilder(activity) 74 | .setType(activity.contentResolver.getType(uri)) 75 | .setChooserTitle(intentTitle) 76 | .setSubject(intentSubject) 77 | .setStream(uri) 78 | .intent 79 | shareIntent.apply { 80 | clipData = ClipData.newRawUri(clipDataLabel, uri) 81 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 82 | } 83 | return Intent.createChooser(shareIntent, intentTitle) 84 | } 85 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/util/ChuckerInterceptorDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.util 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.R 5 | import com.chuckerteam.chucker.api.BodyDecoder 6 | import com.chuckerteam.chucker.api.ChuckerCollector 7 | import com.chuckerteam.chucker.api.ChuckerInterceptor 8 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 9 | import com.chuckerteam.chucker.internal.support.CacheDirectoryProvider 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import okhttp3.Interceptor 13 | import okhttp3.Response 14 | import java.util.concurrent.CopyOnWriteArrayList 15 | import java.util.concurrent.atomic.AtomicLong 16 | 17 | internal class ChuckerInterceptorDelegate( 18 | maxContentLength: Long = 250000L, 19 | headersToRedact: Set = emptySet(), 20 | alwaysReadResponseBody: Boolean = false, 21 | cacheDirectoryProvider: CacheDirectoryProvider, 22 | decoders: List = emptyList(), 23 | skipPaths: List = emptyList() 24 | ) : Interceptor { 25 | private val idGenerator = AtomicLong() 26 | private val transactions = CopyOnWriteArrayList() 27 | 28 | private val mockContext = mockk { 29 | every { getString(R.string.chucker_body_content_truncated) } returns "\n\n--- Content truncated ---" 30 | } 31 | private val mockCollector = mockk { 32 | every { onRequestSent(any()) } returns Unit 33 | every { onResponseReceived(any()) } answers { 34 | val transaction = (args[0] as HttpTransaction) 35 | transaction.id = idGenerator.getAndIncrement() 36 | transactions.add(transaction) 37 | } 38 | } 39 | 40 | private val chucker = ChuckerInterceptor.Builder(context = mockContext) 41 | .collector(mockCollector) 42 | .maxContentLength(maxContentLength) 43 | .redactHeaders(headersToRedact) 44 | .alwaysReadResponseBody(alwaysReadResponseBody) 45 | .cacheDirectorProvider(cacheDirectoryProvider) 46 | .skipPaths(skipPaths = skipPaths.toTypedArray()) 47 | .apply { decoders.forEach(::addBodyDecoder) } 48 | .build() 49 | 50 | internal fun expectTransaction(): HttpTransaction { 51 | if (transactions.isEmpty()) { 52 | throw AssertionError("Expected transaction but was empty") 53 | } 54 | return transactions.removeAt(0) 55 | } 56 | 57 | internal fun expectNoTransactions() { 58 | if (transactions.isNotEmpty()) { 59 | throw AssertionError("Expected no transactions but found ${transactions.size}") 60 | } 61 | } 62 | 63 | override fun intercept(chain: Interceptor.Chain): Response { 64 | return chucker.intercept(chain) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/SearchHighlightUtil.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.text.SpannableStringBuilder 4 | import android.text.Spanned 5 | import android.text.style.BackgroundColorSpan 6 | import android.text.style.ForegroundColorSpan 7 | import android.text.style.UnderlineSpan 8 | import java.util.regex.Pattern 9 | 10 | /** 11 | * Highlight parts of the String when it matches the search. 12 | * 13 | * @param search the text to highlight 14 | */ 15 | internal fun SpannableStringBuilder.highlightWithDefinedColors( 16 | search: String, 17 | startIndices: List, 18 | backgroundColor: Int, 19 | foregroundColor: Int 20 | ): SpannableStringBuilder { 21 | return applyColoredSpannable(this, startIndices, search.length, backgroundColor, foregroundColor) 22 | } 23 | 24 | internal fun CharSequence.indicesOf(input: String): List = 25 | Pattern.compile(input, Pattern.CASE_INSENSITIVE).toRegex() 26 | .findAll(this) 27 | .map { it.range.first } 28 | .toCollection(mutableListOf()) 29 | 30 | internal fun SpannableStringBuilder.highlightWithDefinedColorsSubstring( 31 | search: String, 32 | startIndex: Int, 33 | backgroundColor: Int, 34 | foregroundColor: Int 35 | ): SpannableStringBuilder { 36 | return applyColoredSpannableSubstring(this, startIndex, search.length, backgroundColor, foregroundColor) 37 | } 38 | 39 | private fun applyColoredSpannableSubstring( 40 | text: SpannableStringBuilder, 41 | subStringStartPosition: Int, 42 | subStringLength: Int, 43 | backgroundColor: Int, 44 | foregroundColor: Int 45 | ): SpannableStringBuilder { 46 | return text.apply { 47 | setSpan( 48 | UnderlineSpan(), 49 | subStringStartPosition, 50 | subStringStartPosition + subStringLength, 51 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 52 | ) 53 | setSpan( 54 | ForegroundColorSpan(foregroundColor), 55 | subStringStartPosition, 56 | subStringStartPosition + subStringLength, 57 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 58 | ) 59 | setSpan( 60 | BackgroundColorSpan(backgroundColor), 61 | subStringStartPosition, 62 | subStringStartPosition + subStringLength, 63 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 64 | ) 65 | } 66 | } 67 | 68 | private fun applyColoredSpannable( 69 | text: SpannableStringBuilder, 70 | indexes: List, 71 | length: Int, 72 | backgroundColor: Int, 73 | foregroundColor: Int 74 | ): SpannableStringBuilder { 75 | return text.apply { 76 | indexes.forEach { 77 | applyColoredSpannableSubstring(text, it, length, backgroundColor, foregroundColor) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/LiveDataDistinctUntilChangedTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.distinctUntilChanged 6 | import com.chuckerteam.chucker.util.test 7 | import com.google.common.truth.Truth.assertThat 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | internal class LiveDataDistinctUntilChangedTest { 12 | @get:Rule val instantExecutorRule = InstantTaskExecutorRule() 13 | 14 | @Test 15 | fun `downstream emits initial upstream data`() { 16 | val upstream = MutableLiveData(null) 17 | 18 | upstream.distinctUntilChanged().test { 19 | assertThat(expectData()).isNull() 20 | } 21 | } 22 | 23 | @Test 24 | fun `downstream does not emit if the upstream is empty`() { 25 | val upstream = MutableLiveData() 26 | 27 | upstream.distinctUntilChanged().test { 28 | expectNoData() 29 | } 30 | } 31 | 32 | @Test 33 | fun `downstream emits new values`() { 34 | val upstream = MutableLiveData() 35 | 36 | upstream.distinctUntilChanged().test { 37 | upstream.value = 1 38 | assertThat(expectData()).isEqualTo(1) 39 | 40 | upstream.value = 2 41 | assertThat(expectData()).isEqualTo(2) 42 | 43 | upstream.value = null 44 | assertThat(expectData()).isNull() 45 | 46 | upstream.value = 2 47 | assertThat(expectData()).isEqualTo(2) 48 | } 49 | } 50 | 51 | @Test 52 | fun `downstream does not emit repeated data`() { 53 | val upstream = MutableLiveData() 54 | 55 | upstream.distinctUntilChanged().test { 56 | upstream.value = null 57 | assertThat(expectData()).isNull() 58 | 59 | upstream.value = null 60 | expectNoData() 61 | 62 | upstream.value = "" 63 | assertThat(expectData()).isEmpty() 64 | 65 | upstream.value = "" 66 | expectNoData() 67 | } 68 | } 69 | 70 | @Test 71 | fun `downstream emits according to distinct filter`() { 72 | val upstream = MutableLiveData>() 73 | 74 | upstream.distinctUntilChanged { old, new -> old.first == new.first }.test { 75 | upstream.value = 1 to "" 76 | assertThat(expectData()).isEqualTo(1 to "") 77 | 78 | upstream.value = 1 to "a" 79 | expectNoData() 80 | 81 | upstream.value = 2 to "b" 82 | assertThat(expectData()).isEqualTo(2 to "b") 83 | 84 | upstream.value = 3 to "b" 85 | assertThat(expectData()).isEqualTo(3 to "b") 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/OkHttpUtils.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import okhttp3.Headers 4 | import okhttp3.Response 5 | import okio.Source 6 | import okio.buffer 7 | import okio.gzip 8 | import okio.source 9 | import org.brotli.dec.BrotliInputStream 10 | import java.net.HttpURLConnection.HTTP_NOT_MODIFIED 11 | import java.net.HttpURLConnection.HTTP_NO_CONTENT 12 | import java.net.HttpURLConnection.HTTP_OK 13 | import java.util.Locale 14 | 15 | private const val HTTP_CONTINUE = 100 16 | 17 | /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */ 18 | internal fun Response.hasBody(): Boolean { 19 | // HEAD requests never yield a body regardless of the response headers. 20 | if (request.method == "HEAD") { 21 | return false 22 | } 23 | 24 | val responseCode = code 25 | if ((responseCode < HTTP_CONTINUE || responseCode >= HTTP_OK) && 26 | (responseCode != HTTP_NO_CONTENT) && 27 | (responseCode != HTTP_NOT_MODIFIED) 28 | ) { 29 | return true 30 | } 31 | 32 | // If the Content-Length or Transfer-Encoding headers disagree with the response code, the 33 | // response is malformed. For best compatibility, we honor the headers. 34 | return ((contentLength > 0) || isChunked) 35 | } 36 | 37 | private val Response.contentLength: Long 38 | get() { 39 | return this.header("Content-Length")?.toLongOrNull() ?: -1L 40 | } 41 | 42 | internal val Response.isChunked: Boolean 43 | get() { 44 | return this.header("Transfer-Encoding").equals("chunked", ignoreCase = true) 45 | } 46 | 47 | internal val Response.contentType: String? 48 | get() { 49 | return this.header("Content-Type") 50 | } 51 | 52 | private val Headers.containsGzip: Boolean 53 | get() { 54 | return this["Content-Encoding"].equals("gzip", ignoreCase = true) 55 | } 56 | 57 | private val Headers.containsBrotli: Boolean 58 | get() { 59 | return this["Content-Encoding"].equals("br", ignoreCase = true) 60 | } 61 | 62 | private val supportedEncodings = listOf("identity", "gzip", "br") 63 | 64 | internal val Headers.hasSupportedContentEncoding: Boolean 65 | get() = get("Content-Encoding") 66 | ?.takeIf { it.isNotEmpty() } 67 | ?.let { it.lowercase(Locale.ROOT) in supportedEncodings } 68 | ?: true 69 | 70 | internal fun Source.uncompress(headers: Headers) = when { 71 | headers.containsGzip -> gzip() 72 | headers.containsBrotli -> BrotliInputStream(this.buffer().inputStream()).source() 73 | else -> this 74 | } 75 | 76 | internal fun Headers.redact(names: Iterable): Headers { 77 | val builder = newBuilder() 78 | for (name in names()) { 79 | if (names.any { userHeader -> userHeader.equals(name, ignoreCase = true) }) { 80 | builder[name] = "**" 81 | } 82 | } 83 | return builder.build() 84 | } 85 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/util/TestTransactionFactory.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.util 2 | 3 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 4 | import java.util.Date 5 | 6 | internal object TestTransactionFactory { 7 | 8 | internal fun createTransaction(method: String): HttpTransaction { 9 | return HttpTransaction( 10 | id = 0, 11 | requestDate = Date(1300000).time, 12 | responseDate = Date(1300300).time, 13 | tookMs = 1000L, 14 | protocol = "HTTP", 15 | method = method, 16 | url = "http://localhost:80/getUsers", 17 | host = "localhost", 18 | path = "/getUsers", 19 | scheme = "", 20 | responseTlsVersion = "", 21 | responseCipherSuite = "", 22 | requestPayloadSize = 1000L, 23 | requestContentType = "application/json", 24 | requestHeaders = null, 25 | requestHeadersSize = null, 26 | requestBody = null, 27 | isRequestBodyEncoded = false, 28 | responseCode = 200, 29 | responseMessage = "OK", 30 | error = null, 31 | responsePayloadSize = 1000L, 32 | responseContentType = "application/json", 33 | responseHeaders = null, 34 | responseHeadersSize = null, 35 | responseBody = """{"field": "value"}""", 36 | isResponseBodyEncoded = false, 37 | responseImageData = null, 38 | graphQlDetected = false, 39 | graphQlOperationName = null 40 | ) 41 | } 42 | 43 | val expectedGetHttpTransaction = 44 | """ 45 | URL: http://localhost/getUsers 46 | Method: GET 47 | Protocol: HTTP 48 | Status: Complete 49 | Response: 200 OK 50 | SSL: No 51 | 52 | Request time: ${Date(1300000)} 53 | Response time: ${Date(1300300)} 54 | Duration: 1000 ms 55 | 56 | Request size: 1.0 kB 57 | Response size: 1.0 kB 58 | Total size: 2.0 kB 59 | 60 | ---------- Request ---------- 61 | 62 | (body is empty) 63 | 64 | ---------- Response ---------- 65 | 66 | { 67 | "field": "value" 68 | } 69 | """.trimIndent() 70 | 71 | val expectedHttpPostTransaction = 72 | """ 73 | URL: http://localhost/getUsers 74 | Method: POST 75 | Protocol: HTTP 76 | Status: Complete 77 | Response: 200 OK 78 | SSL: No 79 | 80 | Request time: ${Date(1300000)} 81 | Response time: ${Date(1300300)} 82 | Duration: 1000 ms 83 | 84 | Request size: 1.0 kB 85 | Response size: 1.0 kB 86 | Total size: 2.0 kB 87 | 88 | ---------- Request ---------- 89 | 90 | (body is empty) 91 | 92 | ---------- Response ---------- 93 | 94 | { 95 | "field": "value" 96 | } 97 | """.trimIndent() 98 | } 99 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/data/har/EntryTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.data.har 2 | 3 | import com.chuckerteam.chucker.internal.data.har.log.Entry 4 | import com.chuckerteam.chucker.internal.data.har.log.entry.Request 5 | import com.chuckerteam.chucker.internal.data.har.log.entry.Response 6 | import com.chuckerteam.chucker.internal.data.har.log.entry.Timings 7 | import com.chuckerteam.chucker.internal.data.har.log.entry.request.PostData 8 | import com.chuckerteam.chucker.internal.data.har.log.entry.response.Content 9 | import com.chuckerteam.chucker.util.HarTestUtils 10 | import com.google.common.truth.Truth.assertThat 11 | import org.junit.Test 12 | import java.util.Date 13 | 14 | internal class EntryTest { 15 | @Test 16 | fun `entry is created correctly with start datetime`() { 17 | val transaction = HarTestUtils.createTransaction("GET") 18 | val entry = HarTestUtils.createEntry("GET") 19 | 20 | assertThat(Entry.DateFormat.get()!!.parse(entry!!.startedDateTime)) 21 | .isEqualTo(Date(transaction.requestDate!!)) 22 | } 23 | 24 | @Test 25 | fun `entry is created correctly with time`() { 26 | val entry = HarTestUtils.createEntry("GET") 27 | 28 | assertThat(entry?.time).isEqualTo(1000) 29 | } 30 | 31 | @Test 32 | fun `entry is created correctly with request`() { 33 | val entry = HarTestUtils.createEntry("POST") 34 | 35 | assertThat(entry?.request).isEqualTo( 36 | Request( 37 | method = "POST", 38 | url = "http://localhost:80/getUsers", 39 | httpVersion = "HTTP", 40 | cookies = emptyList(), 41 | headers = emptyList(), 42 | queryString = emptyList(), 43 | postData = PostData(mimeType = "application/json", params = null, text = null), 44 | headersSize = 0, 45 | bodySize = 1000, 46 | totalSize = 1000 47 | ) 48 | ) 49 | } 50 | 51 | @Test 52 | fun `entry is created correctly with response`() { 53 | val entry = HarTestUtils.createEntry("GET") 54 | 55 | assertThat(entry?.response).isEqualTo( 56 | Response( 57 | status = 200, 58 | statusText = "OK", 59 | httpVersion = "HTTP", 60 | cookies = emptyList(), 61 | headers = emptyList(), 62 | content = Content( 63 | size = 1000, 64 | compression = null, 65 | mimeType = "application/json", 66 | text = """{"field": "value"}""", 67 | encoding = null 68 | ), 69 | redirectUrl = "", 70 | headersSize = 0, 71 | bodySize = 1000, 72 | totalSize = 1000 73 | ) 74 | ) 75 | } 76 | 77 | @Test 78 | fun `entry is created correctly with timing`() { 79 | val transaction = HarTestUtils.createTransaction("GET") 80 | val entry = HarTestUtils.createEntry("GET") 81 | 82 | assertThat(entry?.timings).isEqualTo(Timings(transaction)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/chuckerteam/chucker/internal/support/RequestProcessorTest.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.api.BodyDecoder 5 | import com.chuckerteam.chucker.api.ChuckerCollector 6 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import okhttp3.Headers 10 | import okhttp3.HttpUrl.Companion.toHttpUrl 11 | import okhttp3.Request 12 | import org.junit.Assert.assertEquals 13 | import org.junit.Assert.assertFalse 14 | import org.junit.Assert.assertTrue 15 | import org.junit.Test 16 | 17 | internal class RequestProcessorTest { 18 | 19 | private val context: Context = mockk() 20 | private val chuckerCollector: ChuckerCollector = mockk(relaxed = true) 21 | private val maxContentLength: Long = 0 22 | private val headersToRedact: Set = emptySet() 23 | private val bodyDecoders: List = emptyList() 24 | 25 | private val requestProcessor: RequestProcessor = RequestProcessor( 26 | context = context, 27 | collector = chuckerCollector, 28 | maxContentLength = maxContentLength, 29 | headersToRedact = headersToRedact, 30 | bodyDecoders = bodyDecoders 31 | ) 32 | 33 | @Test 34 | fun `GIVEN graphql headers WHEN process request THEN transaction has graphQlOperationName`() { 35 | val operationName = "SearchCharacters" 36 | val transaction = HttpTransaction() 37 | val headersGraphQl = Headers.Builder().add("X-APOLLO-OPERATION-NAME", operationName).build() 38 | val request: Request = mockk(relaxed = true) { 39 | every { headers } returns headersGraphQl 40 | } 41 | 42 | requestProcessor.process(request, transaction) 43 | 44 | assertEquals(operationName, transaction.graphQlOperationName) 45 | assertTrue(transaction.graphQlDetected) 46 | } 47 | 48 | @Test 49 | fun `GIVEN an Url containing graphql path WHEN process request THEN transaction isGraphQLRequest`() { 50 | val transaction = HttpTransaction() 51 | val request: Request = mockk(relaxed = true) { 52 | every { url } returns "http://some/api/graphql".toHttpUrl() 53 | } 54 | requestProcessor.process(request, transaction) 55 | 56 | assertTrue(transaction.graphQlDetected) 57 | } 58 | 59 | @Test 60 | fun `GIVEN a url with no graphql path WHEN process request THEN transaction !isGraphQLRequest`() { 61 | val transaction = HttpTransaction() 62 | val request: Request = mockk(relaxed = true) { 63 | every { url } returns "http://some/random/api".toHttpUrl() 64 | } 65 | requestProcessor.process(request, transaction) 66 | 67 | assertFalse(transaction.graphQlDetected) 68 | } 69 | 70 | @Test 71 | fun `GIVEN a url with graphql host WHEN process request THEN transaction isGraphQLRequest`() { 72 | val transaction = HttpTransaction() 73 | val request: Request = mockk(relaxed = true) { 74 | every { url } returns "http://some.graphql.api".toHttpUrl() 75 | } 76 | requestProcessor.process(request, transaction) 77 | 78 | assertTrue(transaction.graphQlDetected) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'com.squareup.wire' 4 | apply plugin: 'com.apollographql.apollo3' 5 | 6 | wire { 7 | kotlin {} 8 | } 9 | 10 | android { 11 | compileSdk rootProject.compileSdkVersion 12 | namespace "com.chuckerteam.chucker.sample" 13 | 14 | defaultConfig { 15 | minSdk rootProject.minSdkVersion 16 | targetSdk rootProject.targetSdkVersion 17 | applicationId "com.chuckerteam.chucker.sample" 18 | versionName VERSION_NAME 19 | versionCode VERSION_CODE.toInteger() 20 | } 21 | 22 | buildFeatures { 23 | viewBinding true 24 | buildConfig = false 25 | } 26 | 27 | buildTypes { 28 | debug { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 31 | signingConfig signingConfigs.debug 32 | } 33 | release { 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | signingConfig signingConfigs.debug 37 | } 38 | } 39 | 40 | signingConfigs { 41 | debug { 42 | keyAlias 'chucker' 43 | keyPassword 'android' 44 | storeFile file('debug.keystore') 45 | storePassword 'android' 46 | } 47 | } 48 | 49 | lintOptions { 50 | warningsAsErrors true 51 | abortOnError true 52 | disable 'AcceptsUserCertificates' 53 | // Don't fail build if some dependencies outdated 54 | disable 'GradleDependency' 55 | } 56 | 57 | compileOptions { 58 | sourceCompatibility JavaVersion.VERSION_11 59 | targetCompatibility JavaVersion.VERSION_11 60 | } 61 | 62 | kotlin { 63 | jvmToolchain(11) 64 | } 65 | } 66 | 67 | apollo { 68 | service("rickandmortyapi") { 69 | packageName.set("com.chuckerteam.chucker.sample") 70 | schemaFile.set(file("src/main/graphql/com/chuckerteam/chucker/sample/schema.json.graphql")) 71 | srcDir("src/main/graphql") 72 | excludes.add("**/schema.json.graphql") 73 | excludes.add("**/schema.json") 74 | } 75 | } 76 | 77 | dependencies { 78 | debugImplementation project(':library') 79 | releaseImplementation project(':library-no-op') 80 | 81 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 82 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" 83 | implementation "androidx.activity:activity-ktx:$activityVersion" 84 | 85 | implementation "com.google.android.material:material:$materialComponentsVersion" 86 | implementation "androidx.appcompat:appcompat:$appCompatVersion" 87 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" 88 | 89 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" 90 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 91 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 92 | 93 | implementation "com.apollographql.apollo3:apollo-runtime:$apolloVersion" 94 | 95 | debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion" 96 | } 97 | 98 | apply from: rootProject.file('gradle/kotlin-static-analysis.gradle') 99 | -------------------------------------------------------------------------------- /docs/migrating-from-2.0.md: -------------------------------------------------------------------------------- 1 | # Migrating from Chucker 2.x to 3.x 2 | 3 | Please refer to this page if you're **migrating from chucker version `2.0.4` to `3.x`**. 4 | 5 | In this page you will find the summary of all the breaking changes that you potentially need to fix. 6 | 7 | ## 1. Class name changes 8 | 9 | Generally name of classes from Chucker 2.x used to have `Chuck` as a prefix (e.g. `ChuckInterceptor`). In version 3.x we updated the naming of all the classes to have `Chucker` as a prefix (e.g. `ChuckerInterceptor`). This is valid for all the classes in the library. 10 | 11 | So if to launch the UI of Chucker, you would normally call: 12 | 13 | ```kotlin 14 | Chuck.getLaunchIntent(...) 15 | ``` 16 | 17 | now you will call 18 | 19 | ```kotlin 20 | Chucker.getLaunchIntent(...) 21 | ``` 22 | 23 | ## 2. Package name changes 24 | 25 | Please note that with version 3.x package name is also updated. The new package for the classes of Chucker will be `com.chuckerteam.chucker.api`. 26 | 27 | Here a summary of the name/package changes in chucker 28 | 29 | | Old | New | 30 | | --- | --- | 31 | | `com.readystatesoftware.chuck.api.Chuck` | `com.chuckerteam.chucker.api.Chucker` | 32 | | `com.readystatesoftware.chuck.api.ChuckCollector` | `com.chuckerteam.chucker.api.ChuckerCollector` | 33 | | `com.readystatesoftware.chuck.api.ChuckerInterceptor` | `com.chuckerteam.chucker.api.ChuckerInterceptor` | 34 | 35 | ## 3. Update the code to configure the interceptor 36 | 37 | Chucker v2.0 used to use a _Builder_ pattern to configure your interceptor. Chucker v3.0 instead is using _Kotlin named parameters_ with default values to configure the interceptor. Multiple builder methods have been **removed** and you need to replace them with parameters from the constructors. 38 | 39 | ### Java 40 | 41 | The following code: 42 | 43 | ```java 44 | ChuckInterceptor interceptor = new ChuckInterceptor(context, collector) 45 | .maxContentLength(120000L); 46 | ``` 47 | 48 | should be updated to: 49 | 50 | ```java 51 | ChuckInterceptor interceptor = new ChuckInterceptor(context, collector, 120000) 52 | ``` 53 | 54 | ### Kotlin 55 | 56 | We suggest to use Kotlin to configure your interceptor as it makes the code more clean/elegant. 57 | 58 | The following code: 59 | 60 | ```kotlin 61 | val retentionManager = RetentionManager(androidApplication, ChuckCollector.Period.ONE_HOUR) 62 | 63 | val collector = ChuckCollector(androidApplication) 64 | .retentionManager(retentionManager) 65 | .showNotification(true) 66 | 67 | val interceptor = ChuckInterceptor(context, collector) 68 | .maxContentLength(120000L) 69 | ``` 70 | 71 | should be updated to: 72 | 73 | ```kotlin 74 | val collector = ChuckerCollector( 75 | context = this, 76 | showNotification = true, 77 | retentionPeriod = RetentionManager.Period.ONE_HOUR 78 | ) 79 | 80 | val interceptor = ChuckerInterceptor( 81 | context = context, 82 | collector = collector, 83 | maxContentLength = 120000L 84 | ) 85 | ``` 86 | 87 | ## 4. RetentionManager is now replaced by the retentionPeriod 88 | 89 | You don't need to create a `RetentionManager` anymore and you simply have to specify the `retentionPeriod` parameter when creating a `ChuckerCollector`. 90 | 91 | The `Period` enum has also been moved from `ChuckCollector` to `RetentionManager`. 92 | 93 | ## 5. registerDefaultCrashHanlder typo 94 | 95 | The function `Chuck.registerDefaultCrashHanlder` contained a typo in the name and now is moved to `Chucker.registerDefaultCrashHandler`. 96 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/api/Chucker.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.api 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.ShortcutInfo 6 | import android.content.pm.ShortcutManager 7 | import android.graphics.drawable.Icon 8 | import android.os.Build 9 | import android.util.Log 10 | import androidx.core.content.getSystemService 11 | import com.chuckerteam.chucker.R 12 | import com.chuckerteam.chucker.internal.support.Logger 13 | import com.chuckerteam.chucker.internal.support.NotificationHelper 14 | import com.chuckerteam.chucker.internal.ui.MainActivity 15 | 16 | /** 17 | * Chucker methods and utilities to interact with the library. 18 | */ 19 | public object Chucker { 20 | 21 | private const val SHORTCUT_ID = "chuckerShortcutId" 22 | 23 | /** 24 | * Check if this instance is the operation one or no-op. 25 | * @return `true` if this is the operation instance. 26 | */ 27 | @Suppress("MayBeConst ") // https://github.com/ChuckerTeam/chucker/pull/169#discussion_r362341353 28 | public val isOp: Boolean = true 29 | 30 | /** 31 | * Get an Intent to launch the Chucker UI directly. 32 | * @param context An Android [Context]. 33 | * @return An Intent for the main Chucker Activity that can be started with [Context.startActivity]. 34 | */ 35 | @JvmStatic 36 | public fun getLaunchIntent(context: Context): Intent { 37 | return Intent(context, MainActivity::class.java) 38 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 39 | } 40 | 41 | /** 42 | * Create a shortcut to launch Chucker UI. 43 | * @param context An Android [Context]. 44 | */ 45 | internal fun createShortcut(context: Context) { 46 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { 47 | return 48 | } 49 | 50 | val shortcutManager = context.getSystemService() ?: return 51 | if (shortcutManager.dynamicShortcuts.any { it.id == SHORTCUT_ID }) { 52 | return 53 | } 54 | 55 | val shortcut = ShortcutInfo.Builder(context, SHORTCUT_ID) 56 | .setShortLabel(context.getString(R.string.chucker_shortcut_label)) 57 | .setLongLabel(context.getString(R.string.chucker_shortcut_label)) 58 | .setIcon(Icon.createWithResource(context, R.mipmap.chucker_ic_launcher)) 59 | .setIntent(getLaunchIntent(context).setAction(Intent.ACTION_VIEW)) 60 | .build() 61 | try { 62 | shortcutManager.addDynamicShortcuts(listOf(shortcut)) 63 | } catch (e: IllegalArgumentException) { 64 | Logger.warn("ShortcutManager addDynamicShortcuts failed ", e) 65 | } catch (e: IllegalStateException) { 66 | Logger.warn("ShortcutManager addDynamicShortcuts failed ", e) 67 | } 68 | } 69 | 70 | /** 71 | * Dismisses all previous Chucker notifications. 72 | */ 73 | @JvmStatic 74 | public fun dismissNotifications(context: Context) { 75 | NotificationHelper(context).dismissNotifications() 76 | } 77 | 78 | internal var logger: Logger = object : Logger { 79 | val TAG = "Chucker" 80 | 81 | override fun info(message: String, throwable: Throwable?) { 82 | Log.i(TAG, message, throwable) 83 | } 84 | 85 | override fun warn(message: String, throwable: Throwable?) { 86 | Log.w(TAG, message, throwable) 87 | } 88 | 89 | override fun error(message: String, throwable: Throwable?) { 90 | Log.e(TAG, message, throwable) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/chuckerteam/chucker/internal/support/TransactionDetailsSharable.kt: -------------------------------------------------------------------------------- 1 | package com.chuckerteam.chucker.internal.support 2 | 3 | import android.content.Context 4 | import com.chuckerteam.chucker.R 5 | import com.chuckerteam.chucker.internal.data.entity.HttpTransaction 6 | import okio.Buffer 7 | import okio.Source 8 | 9 | internal class TransactionDetailsSharable( 10 | private val transaction: HttpTransaction, 11 | private val encodeUrls: Boolean 12 | ) : Sharable { 13 | override fun toSharableContent(context: Context): Source = Buffer().apply { 14 | writeUtf8("${context.getString(R.string.chucker_url)}: ${transaction.getFormattedUrl(encodeUrls)}\n") 15 | writeUtf8("${context.getString(R.string.chucker_method)}: ${transaction.method}\n") 16 | writeUtf8("${context.getString(R.string.chucker_protocol)}: ${transaction.protocol}\n") 17 | writeUtf8("${context.getString(R.string.chucker_status)}: ${transaction.status}\n") 18 | writeUtf8("${context.getString(R.string.chucker_response)}: ${transaction.responseSummaryText}\n") 19 | val isSsl = if (transaction.isSsl) R.string.chucker_yes else R.string.chucker_no 20 | writeUtf8("${context.getString(R.string.chucker_ssl)}: ${context.getString(isSsl)}\n") 21 | writeUtf8("\n") 22 | writeUtf8("${context.getString(R.string.chucker_request_time)}: ${transaction.requestDateString}\n") 23 | writeUtf8("${context.getString(R.string.chucker_response_time)}: ${transaction.responseDateString}\n") 24 | writeUtf8("${context.getString(R.string.chucker_duration)}: ${transaction.durationString}\n") 25 | writeUtf8("\n") 26 | writeUtf8("${context.getString(R.string.chucker_request_size)}: ${transaction.requestSizeString}\n") 27 | writeUtf8("${context.getString(R.string.chucker_response_size)}: ${transaction.responseSizeString}\n") 28 | writeUtf8("${context.getString(R.string.chucker_total_size)}: ${transaction.totalSizeString}\n") 29 | writeUtf8("\n") 30 | writeUtf8("---------- ${context.getString(R.string.chucker_request)} ----------\n\n") 31 | 32 | var headers = FormatUtils.formatHeaders(transaction.getParsedRequestHeaders(), false) 33 | 34 | if (headers.isNotBlank()) { 35 | writeUtf8(headers) 36 | writeUtf8("\n") 37 | } 38 | 39 | writeUtf8( 40 | if (transaction.requestBody.isNullOrBlank()) { 41 | val resId = if (transaction.isResponseBodyEncoded) { 42 | R.string.chucker_body_omitted 43 | } else { 44 | R.string.chucker_body_empty 45 | } 46 | context.getString(resId) 47 | } else { 48 | transaction.getFormattedRequestBody() 49 | } 50 | ) 51 | 52 | writeUtf8("\n\n") 53 | writeUtf8("---------- ${context.getString(R.string.chucker_response)} ----------\n\n") 54 | 55 | headers = FormatUtils.formatHeaders(transaction.getParsedResponseHeaders(), false) 56 | 57 | if (headers.isNotBlank()) { 58 | writeUtf8(headers) 59 | writeUtf8("\n") 60 | } 61 | 62 | writeUtf8( 63 | if (transaction.responseBody.isNullOrBlank()) { 64 | val resId = if (transaction.isResponseBodyEncoded) { 65 | R.string.chucker_body_omitted 66 | } else { 67 | R.string.chucker_body_empty 68 | } 69 | context.getString(resId) 70 | } else { 71 | transaction.getFormattedResponseBody() 72 | } 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/pre-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Pre Merge Checks 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | pull_request: 7 | branches: 8 | - '*' 9 | workflow_dispatch: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | test: 15 | runs-on: [ubuntu-latest] 16 | 17 | steps: 18 | - name: Cancel Previous Runs 19 | if: github.event_name == 'pull_request' 20 | uses: styfle/cancel-workflow-action@0.12.0 21 | with: 22 | access_token: ${{ github.token }} 23 | 24 | - name: Checkout Repo 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Java 28 | uses: actions/setup-java@v3 29 | with: 30 | distribution: 'zulu' 31 | java-version: '17' 32 | 33 | - name: Setup Gradle 34 | uses: gradle/gradle-build-action@v2 35 | 36 | - name: Run all the tests 37 | run: ./gradlew test 38 | 39 | detekt: 40 | runs-on: [ubuntu-latest] 41 | 42 | steps: 43 | - name: Checkout Repo 44 | uses: actions/checkout@v4 45 | 46 | - name: Setup Java 47 | uses: actions/setup-java@v3 48 | with: 49 | distribution: 'zulu' 50 | java-version: '17' 51 | 52 | - name: Setup Gradle 53 | uses: gradle/gradle-build-action@v2 54 | 55 | - name: Run detekt 56 | run: ./gradlew detekt 57 | 58 | lint: 59 | runs-on: [ubuntu-latest] 60 | 61 | steps: 62 | - name: Checkout Repo 63 | uses: actions/checkout@v4 64 | 65 | - name: Setup Java 66 | uses: actions/setup-java@v3 67 | with: 68 | distribution: 'zulu' 69 | java-version: '17' 70 | 71 | - name: Setup Gradle 72 | uses: gradle/gradle-build-action@v2 73 | 74 | - name: Run lint 75 | run: ./gradlew lint 76 | 77 | ktlint: 78 | runs-on: [ubuntu-latest] 79 | 80 | steps: 81 | - name: Checkout Repo 82 | uses: actions/checkout@v4 83 | 84 | - name: Setup Java 85 | uses: actions/setup-java@v3 86 | with: 87 | distribution: 'zulu' 88 | java-version: '17' 89 | 90 | - name: Setup Gradle 91 | uses: gradle/gradle-build-action@v2 92 | 93 | - name: Run ktlint 94 | run: ./gradlew ktlintCheck 95 | 96 | api-check: 97 | runs-on: [ubuntu-latest] 98 | 99 | steps: 100 | - name: Checkout Repo 101 | uses: actions/checkout@v4 102 | 103 | - name: Setup Java 104 | uses: actions/setup-java@v3 105 | with: 106 | distribution: 'zulu' 107 | java-version: '17' 108 | 109 | - name: Setup Gradle 110 | uses: gradle/gradle-build-action@v2 111 | 112 | - name: Run apiCheck 113 | run: ./gradlew apiCheck 114 | 115 | publish-artifact: 116 | runs-on: [ubuntu-latest] 117 | 118 | steps: 119 | - name: Checkout Repo 120 | uses: actions/checkout@v4 121 | 122 | - name: Setup Java 123 | uses: actions/setup-java@v3 124 | with: 125 | distribution: 'zulu' 126 | java-version: '17' 127 | 128 | - name: Setup Gradle 129 | uses: gradle/gradle-build-action@v2 130 | 131 | - name: Publish to Maven Local 132 | run: ./gradlew publishToMavenLocal 133 | env: 134 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} 135 | ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} 136 | 137 | - name: Upload Build Artifacts 138 | uses: actions/upload-artifact@v3 139 | with: 140 | name: 'chucker-local-artifacts' 141 | path: '~/.m2/repository/' 142 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chucker_activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 24 | 25 | 26 | 27 | 39 | 40 | 46 | 47 | 56 | 57 | 66 | 67 | 76 | 77 | 78 | --------------------------------------------------------------------------------