├── .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 |
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 |
21 |
22 |
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 |
--------------------------------------------------------------------------------