├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── assets ├── chuck.gif └── multiwindow.gif ├── build.gradle ├── gradle.properties ├── gradle ├── gradle-mvn-push.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ic_launcher-web.png ├── library-no-op ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── readystatesoftware │ └── chuck │ ├── Chuck.java │ └── ChuckInterceptor.java ├── library ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── readystatesoftware │ │ └── chuck │ │ ├── Chuck.java │ │ ├── ChuckInterceptor.java │ │ └── internal │ │ ├── data │ │ ├── ChuckContentProvider.java │ │ ├── ChuckDbOpenHelper.java │ │ ├── HttpHeader.java │ │ ├── HttpTransaction.java │ │ └── LocalCupboard.java │ │ ├── support │ │ ├── ClearTransactionsService.java │ │ ├── FormatUtils.java │ │ ├── JsonConvertor.java │ │ ├── NotificationHelper.java │ │ ├── RetentionManager.java │ │ ├── SQLiteUtils.java │ │ └── SimpleOnPageChangedListener.java │ │ └── ui │ │ ├── BaseChuckActivity.java │ │ ├── MainActivity.java │ │ ├── TransactionActivity.java │ │ ├── TransactionAdapter.java │ │ ├── TransactionFragment.java │ │ ├── TransactionListFragment.java │ │ ├── TransactionOverviewFragment.java │ │ └── TransactionPayloadFragment.java │ └── res │ ├── drawable │ ├── chuck_ic_delete_white_24dp.xml │ ├── chuck_ic_https_grey_24dp.xml │ ├── chuck_ic_notification_white_24dp.xml │ ├── chuck_ic_search_white_24dp.xml │ └── chuck_ic_share_white_24dp.xml │ ├── layout │ ├── chuck_activity_main.xml │ ├── chuck_activity_transaction.xml │ ├── chuck_fragment_transaction_list.xml │ ├── chuck_fragment_transaction_overview.xml │ ├── chuck_fragment_transaction_payload.xml │ └── chuck_list_item_transaction.xml │ ├── menu │ ├── chuck_main.xml │ └── chuck_transaction.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── readystatesoftware │ │ └── chuck │ │ └── sample │ │ ├── MainActivity.java │ │ └── SampleApiService.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea 3 | .gradle 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 1.1.0 *(2017-08-06)* 5 | ---------------------------- 6 | 7 | * Fix: Supports apps targeting Android O (API 26). 8 | 9 | Version 1.0.4 *(2017-02-22)* 10 | ---------------------------- 11 | 12 | * New: Displays uncompressed gzip encoded request/response bodies when used as a network interceptor. 13 | 14 | Version 1.0.3 *(2017-02-14)* 15 | ---------------------------- 16 | 17 | * New: Adds a maximum content length threshold, beyond which bodies are truncated. 18 | * New: Adds a data retention length property and cleanup task. 19 | * New: Adds a clear action to the notification. 20 | * Fix: Mitigates against CursorWindow blowout when transactions are large. 21 | 22 | Version 1.0.2 *(2017-02-10)* 23 | ---------------------------- 24 | 25 | * Fix: Added Proguard rule for compat SearchView. 26 | * Fix: Null search query displaying invalid results. 27 | 28 | Version 1.0.1 *(2017-02-09)* 29 | ---------------------------- 30 | 31 | * New: Adds a search action which filters on request path or response code. 32 | * New: Adds a transaction count to the notification. 33 | * Fix: Limits the size of the static transaction buffer correctly. 34 | 35 | Version 1.0.0 *(2017-02-07)* 36 | ---------------------------- 37 | 38 | Initial release. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Chuck 2 | ===== 3 | 4 | Chuck is a simple in-app HTTP inspector for Android OkHttp clients. Chuck intercepts and persists all HTTP requests and responses inside your application, and provides a UI for inspecting their content. 5 | 6 | ![Chuck](assets/chuck.gif) 7 | 8 | Apps using Chuck will display a notification showing a summary of ongoing HTTP activity. Tapping on the notification launches the full Chuck UI. Apps can optionally suppress the notification, and launch the Chuck UI directly from within their own interface. HTTP interactions and their contents can be exported via a share intent. 9 | 10 | The main Chuck activity is launched in its own task, allowing it to be displayed alongside the host app UI using Android 7.x multi-window support. 11 | 12 | ![Multi-Window](assets/multiwindow.gif) 13 | 14 | Chuck requires Android 4.1+ and OkHttp 3.x. 15 | 16 | **Warning**: The data generated and stored when using this interceptor may contain sensitive information such as Authorization or Cookie headers, and the contents of request and response bodies. It is intended for use during development, and not in release builds or other production deployments. 17 | 18 | Setup 19 | ----- 20 | 21 | Add the dependency in your `build.gradle` file. Add it alongside the `no-op` variant to isolate Chuck from release builds as follows: 22 | 23 | ```gradle 24 | dependencies { 25 | debugCompile 'com.readystatesoftware.chuck:library:1.1.0' 26 | releaseCompile 'com.readystatesoftware.chuck:library-no-op:1.1.0' 27 | } 28 | ``` 29 | 30 | In your application code, create an instance of `ChuckInterceptor` (you'll need to provide it with a `Context`, because Android) and add it as an interceptor when building your OkHttp client: 31 | 32 | ```java 33 | OkHttpClient client = new OkHttpClient.Builder() 34 | .addInterceptor(new ChuckInterceptor(context)) 35 | .build(); 36 | ``` 37 | 38 | That's it! Chuck will now record all HTTP interactions made by your OkHttp client. You can optionally disable the notification by calling `showNotification(false)` on the interceptor instance, and launch the Chuck UI directly within your app with the intent from `Chuck.getLaunchIntent()`. 39 | 40 | FAQ 41 | --- 42 | 43 | - Why are some of my request headers missing? 44 | - Why are retries and redirects not being captured discretely? 45 | - Why are my encoded request/response bodies not appearing as plain text? 46 | 47 | Please refer to [this section of the OkHttp wiki](https://github.com/square/okhttp/wiki/Interceptors#choosing-between-application-and-network-interceptors). You can choose to use Chuck as either an application or network interceptor, depending on your requirements. 48 | 49 | Acknowledgements 50 | ---------------- 51 | 52 | Chuck uses the following open source libraries: 53 | 54 | - [OkHttp](https://github.com/square/okhttp) - Copyright Square, Inc. 55 | - [Gson](https://github.com/google/gson) - Copyright Google Inc. 56 | - [Cupboard](https://bitbucket.org/littlerobots/cupboard) - Copyright Little Robots. 57 | 58 | License 59 | ------- 60 | 61 | Copyright (C) 2017 Jeff Gilfelt. 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /assets/chuck.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spuermax/chuck/152f9a79f94ea23e9f0542137765961918909a76/assets/chuck.gif -------------------------------------------------------------------------------- /assets/multiwindow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spuermax/chuck/152f9a79f94ea23e9f0542137765961918909a76/assets/multiwindow.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | version = VERSION_NAME 17 | group = GROUP 18 | 19 | repositories { 20 | jcenter() 21 | maven { 22 | url "https://maven.google.com" 23 | } 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | 31 | ext { 32 | minSdkVersion = 16 33 | targetSdkVersion = 26 34 | compileSdkVersion = 26 35 | buildToolsVersion = '26.0.1' 36 | 37 | supportLibVersion = '25.3.1' 38 | okhttp3Version = '3.6.0' 39 | } -------------------------------------------------------------------------------- /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 | VERSION_NAME=1.1.0 20 | VERSION_CODE=110 21 | GROUP=com.readystatesoftware.chuck 22 | 23 | POM_DESCRIPTION=Android in-app HTTP inspector 24 | POM_URL=https://github.com/jgilfelt/chuck 25 | POM_SCM_URL=https://github.com/jgilfelt/chuck 26 | POM_SCM_CONNECTION=scm:git@github.com:jgilfelt/chuck.git 27 | POM_SCM_DEV_CONNECTION=scm:git@github.com:jgilfelt/chuck.git 28 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 29 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 30 | POM_LICENCE_DIST=repo 31 | POM_DEVELOPER_ID=jgilfelt 32 | POM_DEVELOPER_NAME=Jeff Gilfelt -------------------------------------------------------------------------------- /gradle/gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def isReleaseBuild() { 5 | return VERSION_NAME.contains("SNAPSHOT") == false 6 | } 7 | 8 | def getReleaseRepositoryUrl() { 9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 11 | } 12 | 13 | def getSnapshotRepositoryUrl() { 14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 15 | : "https://oss.sonatype.org/content/repositories/snapshots/" 16 | } 17 | 18 | def getRepositoryUsername() { 19 | return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" 20 | } 21 | 22 | def getRepositoryPassword() { 23 | return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" 24 | } 25 | 26 | afterEvaluate { project -> 27 | uploadArchives { 28 | repositories { 29 | mavenDeployer { 30 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 31 | 32 | pom.groupId = GROUP 33 | pom.artifactId = POM_ARTIFACT_ID 34 | pom.version = VERSION_NAME 35 | 36 | repository(url: getReleaseRepositoryUrl()) { 37 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 38 | } 39 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 40 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 41 | } 42 | 43 | pom.project { 44 | name POM_NAME 45 | packaging POM_PACKAGING 46 | description POM_DESCRIPTION 47 | url POM_URL 48 | 49 | scm { 50 | url POM_SCM_URL 51 | connection POM_SCM_CONNECTION 52 | developerConnection POM_SCM_DEV_CONNECTION 53 | } 54 | 55 | licenses { 56 | license { 57 | name POM_LICENCE_NAME 58 | url POM_LICENCE_URL 59 | distribution POM_LICENCE_DIST 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | id POM_DEVELOPER_ID 66 | name POM_DEVELOPER_NAME 67 | } 68 | } 69 | } 70 | 71 | } 72 | } 73 | } 74 | 75 | signing { 76 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 77 | sign configurations.archives 78 | } 79 | 80 | task androidJavadocs(type: Javadoc) { 81 | failOnError = false 82 | source = android.sourceSets.main.java.srcDirs 83 | ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" 84 | classpath += files(ext.androidJar) 85 | } 86 | 87 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 88 | classifier = 'javadoc' 89 | from androidJavadocs.destinationDir 90 | } 91 | 92 | task androidSourcesJar(type: Jar) { 93 | classifier = 'sources' 94 | from android.sourceSets.main.java.sourceFiles 95 | } 96 | 97 | if (JavaVersion.current().isJava8Compatible()) { 98 | allprojects { 99 | tasks.withType(Javadoc) { 100 | options.addStringOption('Xdoclint:none', '-quiet') 101 | } 102 | } 103 | } 104 | 105 | artifacts { 106 | archives androidSourcesJar 107 | archives androidJavadocsJar 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spuermax/chuck/152f9a79f94ea23e9f0542137765961918909a76/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 15 11:04:59 GMT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 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 %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="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 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spuermax/chuck/152f9a79f94ea23e9f0542137765961918909a76/ic_launcher-web.png -------------------------------------------------------------------------------- /library-no-op/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library-no-op/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | } 10 | } 11 | 12 | dependencies { 13 | compile "com.squareup.okhttp3:okhttp:$okhttp3Version" 14 | } 15 | 16 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') -------------------------------------------------------------------------------- /library-no-op/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=library-no-op 2 | POM_NAME=Chuck no-op 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library-no-op/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /library-no-op/src/main/java/com/readystatesoftware/chuck/Chuck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | 21 | /** 22 | * No-op implementation. 23 | */ 24 | public class Chuck { 25 | 26 | public static Intent getLaunchIntent(Context context) { 27 | return new Intent(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library-no-op/src/main/java/com/readystatesoftware/chuck/ChuckInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck; 17 | 18 | import android.content.Context; 19 | 20 | import java.io.IOException; 21 | 22 | import okhttp3.Interceptor; 23 | import okhttp3.Request; 24 | import okhttp3.Response; 25 | 26 | /** 27 | * No-op implementation. 28 | */ 29 | public final class ChuckInterceptor implements Interceptor { 30 | 31 | public enum Period { 32 | ONE_HOUR, 33 | ONE_DAY, 34 | ONE_WEEK, 35 | FOREVER 36 | } 37 | 38 | public ChuckInterceptor(Context context) { 39 | } 40 | 41 | public ChuckInterceptor showNotification(boolean show) { 42 | return this; 43 | } 44 | 45 | public ChuckInterceptor maxContentLength(long max) { 46 | return this; 47 | } 48 | 49 | public ChuckInterceptor retainDataFor(Period period) { 50 | return this; 51 | } 52 | 53 | @Override 54 | public Response intercept(Chain chain) throws IOException { 55 | Request request = chain.request(); 56 | return chain.proceed(request); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | consumerProguardFiles 'proguard-rules.pro' 10 | } 11 | } 12 | 13 | dependencies { 14 | compile 'com.google.code.gson:gson:2.8.0' 15 | compile "com.squareup.okhttp3:okhttp:$okhttp3Version" 16 | compile 'nl.qbusict:cupboard:2.2.0' 17 | compile "com.android.support:design:$supportLibVersion" 18 | } 19 | 20 | apply from: rootProject.file('gradle/gradle-mvn-push.gradle') 21 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=library 2 | POM_NAME=Chuck 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.readystatesoftware.chuck.internal.data.HttpTransaction { *; } 2 | -keep class android.support.v7.widget.SearchView { *; } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 24 | 30 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/Chuck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | 21 | import com.readystatesoftware.chuck.internal.ui.MainActivity; 22 | 23 | /** 24 | * Chuck utilities. 25 | */ 26 | public class Chuck { 27 | 28 | /** 29 | * Get an Intent to launch the Chuck UI directly. 30 | * 31 | * @param context A Context. 32 | * @return An Intent for the main Chuck Activity that can be started with {@link Context#startActivity(Intent)}. 33 | */ 34 | public static Intent getLaunchIntent(Context context) { 35 | return new Intent(context, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 36 | } 37 | } -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/ChuckInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc, 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck; 17 | 18 | import android.content.ContentValues; 19 | import android.content.Context; 20 | import android.net.Uri; 21 | import android.util.Log; 22 | 23 | import com.readystatesoftware.chuck.internal.data.ChuckContentProvider; 24 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 25 | import com.readystatesoftware.chuck.internal.data.LocalCupboard; 26 | import com.readystatesoftware.chuck.internal.support.NotificationHelper; 27 | import com.readystatesoftware.chuck.internal.support.RetentionManager; 28 | 29 | import java.io.EOFException; 30 | import java.io.IOException; 31 | import java.nio.charset.Charset; 32 | import java.nio.charset.UnsupportedCharsetException; 33 | import java.util.Date; 34 | import java.util.concurrent.TimeUnit; 35 | 36 | import okhttp3.Headers; 37 | import okhttp3.Interceptor; 38 | import okhttp3.MediaType; 39 | import okhttp3.Request; 40 | import okhttp3.RequestBody; 41 | import okhttp3.Response; 42 | import okhttp3.ResponseBody; 43 | import okhttp3.internal.http.HttpHeaders; 44 | import okio.Buffer; 45 | import okio.BufferedSource; 46 | import okio.GzipSource; 47 | import okio.Okio; 48 | 49 | /** 50 | * An OkHttp Interceptor which persists and displays HTTP activity in your application for later inspection. 51 | */ 52 | public final class ChuckInterceptor implements Interceptor { 53 | 54 | public enum Period { 55 | /** 56 | * Retain data for the last hour. 57 | */ 58 | ONE_HOUR, 59 | /** 60 | * Retain data for the last day. 61 | */ 62 | ONE_DAY, 63 | /** 64 | * Retain data for the last week. 65 | */ 66 | ONE_WEEK, 67 | /** 68 | * Retain data forever. 69 | */ 70 | FOREVER 71 | } 72 | 73 | private static final String LOG_TAG = "ChuckInterceptor"; 74 | private static final Period DEFAULT_RETENTION = Period.ONE_WEEK; 75 | private static final Charset UTF8 = Charset.forName("UTF-8"); 76 | 77 | private final Context context; 78 | private final NotificationHelper notificationHelper; 79 | private RetentionManager retentionManager; 80 | private boolean showNotification; 81 | private long maxContentLength = 250000L; 82 | 83 | /** 84 | * @param context The current Context. 85 | */ 86 | public ChuckInterceptor(Context context) { 87 | this.context = context.getApplicationContext(); 88 | notificationHelper = new NotificationHelper(this.context); 89 | showNotification = true; 90 | retentionManager = new RetentionManager(this.context, DEFAULT_RETENTION); 91 | } 92 | 93 | /** 94 | * Control whether a notification is shown while HTTP activity is recorded. 95 | * 96 | * @param show true to show a notification, false to suppress it. 97 | * @return The {@link ChuckInterceptor} instance. 98 | */ 99 | public ChuckInterceptor showNotification(boolean show) { 100 | showNotification = show; 101 | return this; 102 | } 103 | 104 | /** 105 | * Set the maximum length for request and response content before it is truncated. 106 | * Warning: setting this value too high may cause unexpected results. 107 | * 108 | * @param max the maximum length (in bytes) for request/response content. 109 | * @return The {@link ChuckInterceptor} instance. 110 | */ 111 | public ChuckInterceptor maxContentLength(long max) { 112 | this.maxContentLength = max; 113 | return this; 114 | } 115 | 116 | /** 117 | * Set the retention period for HTTP transaction data captured by this interceptor. 118 | * The default is one week. 119 | * 120 | * @param period the peroid for which to retain HTTP transaction data. 121 | * @return The {@link ChuckInterceptor} instance. 122 | */ 123 | public ChuckInterceptor retainDataFor(Period period) { 124 | retentionManager = new RetentionManager(context, period); 125 | return this; 126 | } 127 | 128 | @Override public Response intercept(Chain chain) throws IOException { 129 | Request request = chain.request(); 130 | 131 | RequestBody requestBody = request.body(); 132 | boolean hasRequestBody = requestBody != null; 133 | 134 | HttpTransaction transaction = new HttpTransaction(); 135 | transaction.setRequestDate(new Date()); 136 | 137 | transaction.setMethod(request.method()); 138 | transaction.setUrl(request.url().toString()); 139 | 140 | transaction.setRequestHeaders(request.headers()); 141 | if (hasRequestBody) { 142 | if (requestBody.contentType() != null) { 143 | transaction.setRequestContentType(requestBody.contentType().toString()); 144 | } 145 | if (requestBody.contentLength() != -1) { 146 | transaction.setRequestContentLength(requestBody.contentLength()); 147 | } 148 | } 149 | 150 | transaction.setRequestBodyIsPlainText(!bodyHasUnsupportedEncoding(request.headers())); 151 | if (hasRequestBody && transaction.requestBodyIsPlainText()) { 152 | BufferedSource source = getNativeSource(new Buffer(), bodyGzipped(request.headers())); 153 | Buffer buffer = source.buffer(); 154 | requestBody.writeTo(buffer); 155 | Charset charset = UTF8; 156 | MediaType contentType = requestBody.contentType(); 157 | if (contentType != null) { 158 | charset = contentType.charset(UTF8); 159 | } 160 | if (isPlaintext(buffer)) { 161 | transaction.setRequestBody(readFromBuffer(buffer, charset)); 162 | } else { 163 | transaction.setResponseBodyIsPlainText(false); 164 | } 165 | } 166 | 167 | Uri transactionUri = create(transaction); 168 | 169 | long startNs = System.nanoTime(); 170 | Response response; 171 | try { 172 | response = chain.proceed(request); 173 | } catch (Exception e) { 174 | transaction.setError(e.toString()); 175 | update(transaction, transactionUri); 176 | throw e; 177 | } 178 | long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); 179 | 180 | ResponseBody responseBody = response.body(); 181 | 182 | transaction.setRequestHeaders(response.request().headers()); // includes headers added later in the chain 183 | transaction.setResponseDate(new Date()); 184 | transaction.setTookMs(tookMs); 185 | transaction.setProtocol(response.protocol().toString()); 186 | transaction.setResponseCode(response.code()); 187 | transaction.setResponseMessage(response.message()); 188 | 189 | transaction.setResponseContentLength(responseBody.contentLength()); 190 | if (responseBody.contentType() != null) { 191 | transaction.setResponseContentType(responseBody.contentType().toString()); 192 | } 193 | transaction.setResponseHeaders(response.headers()); 194 | 195 | transaction.setResponseBodyIsPlainText(!bodyHasUnsupportedEncoding(response.headers())); 196 | if (HttpHeaders.hasBody(response) && transaction.responseBodyIsPlainText()) { 197 | BufferedSource source = getNativeSource(response); 198 | source.request(Long.MAX_VALUE); 199 | Buffer buffer = source.buffer(); 200 | Charset charset = UTF8; 201 | MediaType contentType = responseBody.contentType(); 202 | if (contentType != null) { 203 | try { 204 | charset = contentType.charset(UTF8); 205 | } catch (UnsupportedCharsetException e) { 206 | update(transaction, transactionUri); 207 | return response; 208 | } 209 | } 210 | if (isPlaintext(buffer)) { 211 | transaction.setResponseBody(readFromBuffer(buffer.clone(), charset)); 212 | } else { 213 | transaction.setResponseBodyIsPlainText(false); 214 | } 215 | transaction.setResponseContentLength(buffer.size()); 216 | } 217 | 218 | update(transaction, transactionUri); 219 | 220 | return response; 221 | } 222 | 223 | private Uri create(HttpTransaction transaction) { 224 | ContentValues values = LocalCupboard.getInstance().withEntity(HttpTransaction.class).toContentValues(transaction); 225 | Uri uri = context.getContentResolver().insert(ChuckContentProvider.TRANSACTION_URI, values); 226 | transaction.setId(Long.valueOf(uri.getLastPathSegment())); 227 | if (showNotification) { 228 | notificationHelper.show(transaction); 229 | } 230 | retentionManager.doMaintenance(); 231 | return uri; 232 | } 233 | 234 | private int update(HttpTransaction transaction, Uri uri) { 235 | ContentValues values = LocalCupboard.getInstance().withEntity(HttpTransaction.class).toContentValues(transaction); 236 | int updated = context.getContentResolver().update(uri, values, null, null); 237 | if (showNotification && updated > 0) { 238 | notificationHelper.show(transaction); 239 | } 240 | return updated; 241 | } 242 | 243 | /** 244 | * Returns true if the body in question probably contains human readable text. Uses a small sample 245 | * of code points to detect unicode control characters commonly used in binary file signatures. 246 | */ 247 | private boolean isPlaintext(Buffer buffer) { 248 | try { 249 | Buffer prefix = new Buffer(); 250 | long byteCount = buffer.size() < 64 ? buffer.size() : 64; 251 | buffer.copyTo(prefix, 0, byteCount); 252 | for (int i = 0; i < 16; i++) { 253 | if (prefix.exhausted()) { 254 | break; 255 | } 256 | int codePoint = prefix.readUtf8CodePoint(); 257 | if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { 258 | return false; 259 | } 260 | } 261 | return true; 262 | } catch (EOFException e) { 263 | return false; // Truncated UTF-8 sequence. 264 | } 265 | } 266 | 267 | private boolean bodyHasUnsupportedEncoding(Headers headers) { 268 | String contentEncoding = headers.get("Content-Encoding"); 269 | return contentEncoding != null && 270 | !contentEncoding.equalsIgnoreCase("identity") && 271 | !contentEncoding.equalsIgnoreCase("gzip"); 272 | } 273 | 274 | private boolean bodyGzipped(Headers headers) { 275 | String contentEncoding = headers.get("Content-Encoding"); 276 | return "gzip".equalsIgnoreCase(contentEncoding); 277 | } 278 | 279 | private String readFromBuffer(Buffer buffer, Charset charset) { 280 | long bufferSize = buffer.size(); 281 | long maxBytes = Math.min(bufferSize, maxContentLength); 282 | String body = ""; 283 | try { 284 | body = buffer.readString(maxBytes, charset); 285 | } catch (EOFException e) { 286 | body += context.getString(R.string.chuck_body_unexpected_eof); 287 | } 288 | if (bufferSize > maxContentLength) { 289 | body += context.getString(R.string.chuck_body_content_truncated); 290 | } 291 | return body; 292 | } 293 | 294 | private BufferedSource getNativeSource(BufferedSource input, boolean isGzipped) { 295 | if (isGzipped) { 296 | GzipSource source = new GzipSource(input); 297 | return Okio.buffer(source); 298 | } else { 299 | return input; 300 | } 301 | } 302 | 303 | private BufferedSource getNativeSource(Response response) throws IOException { 304 | if (bodyGzipped(response.headers())) { 305 | BufferedSource source = response.peekBody(maxContentLength).source(); 306 | if (source.buffer().size() < maxContentLength) { 307 | return getNativeSource(source, true); 308 | } else { 309 | Log.w(LOG_TAG, "gzip encoded response was too long"); 310 | } 311 | } 312 | return response.body().source(); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/data/ChuckContentProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.data; 17 | 18 | import android.content.ContentProvider; 19 | import android.content.ContentUris; 20 | import android.content.ContentValues; 21 | import android.content.Context; 22 | import android.content.UriMatcher; 23 | import android.content.pm.ProviderInfo; 24 | import android.database.Cursor; 25 | import android.database.sqlite.SQLiteDatabase; 26 | import android.net.Uri; 27 | import android.support.annotation.NonNull; 28 | import android.support.annotation.Nullable; 29 | 30 | public class ChuckContentProvider extends ContentProvider { 31 | 32 | public static Uri TRANSACTION_URI; 33 | 34 | private static final int TRANSACTION = 0; 35 | private static final int TRANSACTIONS = 1; 36 | private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 37 | 38 | private ChuckDbOpenHelper databaseHelper; 39 | 40 | @Override 41 | public void attachInfo(Context context, ProviderInfo info) { 42 | super.attachInfo(context, info); 43 | TRANSACTION_URI = Uri.parse("content://" + info.authority + "/transaction"); 44 | matcher.addURI(info.authority, "transaction/#", TRANSACTION); 45 | matcher.addURI(info.authority, "transaction", TRANSACTIONS); 46 | } 47 | 48 | @Override 49 | public boolean onCreate() { 50 | databaseHelper = new ChuckDbOpenHelper(getContext()); 51 | return true; 52 | } 53 | 54 | @Override 55 | @Nullable 56 | public Cursor query(@NonNull Uri uri, @Nullable String[] projection, 57 | @Nullable String selection, @Nullable String[] selectionArgs, 58 | @Nullable String sortOrder) { 59 | SQLiteDatabase db = databaseHelper.getWritableDatabase(); 60 | Cursor cursor = null; 61 | switch (matcher.match(uri)) { 62 | case TRANSACTIONS: 63 | cursor = LocalCupboard.getInstance().withDatabase(db).query(HttpTransaction.class). 64 | withProjection(projection). 65 | withSelection(selection, selectionArgs). 66 | orderBy(sortOrder). 67 | getCursor(); 68 | break; 69 | case TRANSACTION: 70 | cursor = LocalCupboard.getInstance().withDatabase(db).query(HttpTransaction.class). 71 | byId(ContentUris.parseId(uri)). 72 | getCursor(); 73 | break; 74 | } 75 | if (cursor != null) { 76 | cursor.setNotificationUri(getContext().getContentResolver(), uri); 77 | } 78 | return cursor; 79 | } 80 | 81 | @Override 82 | @Nullable 83 | public String getType(@NonNull Uri uri) { 84 | return null; 85 | } 86 | 87 | @Override 88 | @Nullable 89 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { 90 | SQLiteDatabase db = databaseHelper.getWritableDatabase(); 91 | switch (matcher.match(uri)) { 92 | case TRANSACTIONS: 93 | long id = db.insert(LocalCupboard.getInstance().getTable(HttpTransaction.class), null, contentValues); 94 | if (id > 0) { 95 | getContext().getContentResolver().notifyChange(uri, null); 96 | return ContentUris.withAppendedId(TRANSACTION_URI, id); 97 | } 98 | } 99 | return null; 100 | } 101 | 102 | @Override 103 | public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 104 | SQLiteDatabase db = databaseHelper.getWritableDatabase(); 105 | int result = 0; 106 | switch (matcher.match(uri)) { 107 | case TRANSACTIONS: 108 | result = db.delete(LocalCupboard.getInstance().getTable(HttpTransaction.class), selection, selectionArgs); 109 | break; 110 | case TRANSACTION: 111 | result = db.delete(LocalCupboard.getInstance().getTable(HttpTransaction.class), 112 | "_id = ?", new String[]{ uri.getPathSegments().get(1) }); 113 | break; 114 | } 115 | if (result > 0) { 116 | getContext().getContentResolver().notifyChange(uri, null); 117 | } 118 | return result; 119 | } 120 | 121 | @Override 122 | public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, 123 | @Nullable String selection, @Nullable String[] selectionArgs) { 124 | SQLiteDatabase db = databaseHelper.getWritableDatabase(); 125 | int result = 0; 126 | switch (matcher.match(uri)) { 127 | case TRANSACTIONS: 128 | result = db.update(LocalCupboard.getInstance().getTable(HttpTransaction.class), contentValues, selection, selectionArgs); 129 | break; 130 | case TRANSACTION: 131 | result = db.update(LocalCupboard.getInstance().getTable(HttpTransaction.class), contentValues, 132 | "_id = ?", new String[]{ uri.getPathSegments().get(1) }); 133 | break; 134 | } 135 | if (result > 0) { 136 | getContext().getContentResolver().notifyChange(uri, null); 137 | } 138 | return result; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/data/ChuckDbOpenHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.data; 17 | 18 | import android.content.Context; 19 | import android.database.sqlite.SQLiteDatabase; 20 | import android.database.sqlite.SQLiteOpenHelper; 21 | 22 | class ChuckDbOpenHelper extends SQLiteOpenHelper { 23 | 24 | private static final String DATABASE_NAME = "chuck.db"; 25 | private static final int VERSION = 3; 26 | 27 | ChuckDbOpenHelper(Context context) { 28 | super(context, DATABASE_NAME, null, VERSION); 29 | } 30 | 31 | @Override 32 | public void onCreate(SQLiteDatabase db) { 33 | LocalCupboard.getAnnotatedInstance().withDatabase(db).createTables(); 34 | } 35 | 36 | @Override 37 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 38 | LocalCupboard.getAnnotatedInstance().withDatabase(db).upgradeTables(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/data/HttpHeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.data; 17 | 18 | public class HttpHeader { 19 | 20 | private final String name; 21 | private final String value; 22 | 23 | HttpHeader(String name, String value) { 24 | this.name = name; 25 | this.value = value; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public String getValue() { 33 | return value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/data/HttpTransaction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.data; 17 | 18 | import android.net.Uri; 19 | 20 | import com.google.gson.reflect.TypeToken; 21 | import com.readystatesoftware.chuck.internal.support.FormatUtils; 22 | import com.readystatesoftware.chuck.internal.support.JsonConvertor; 23 | 24 | import java.text.SimpleDateFormat; 25 | import java.util.ArrayList; 26 | import java.util.Date; 27 | import java.util.List; 28 | import java.util.Locale; 29 | 30 | import nl.qbusict.cupboard.annotation.Index; 31 | import okhttp3.Headers; 32 | 33 | public class HttpTransaction { 34 | 35 | public enum Status { 36 | Requested, 37 | Complete, 38 | Failed 39 | } 40 | 41 | public static final String[] PARTIAL_PROJECTION = new String[] { 42 | "_id", 43 | "requestDate", 44 | "tookMs", 45 | "method", 46 | "host", 47 | "path", 48 | "scheme", 49 | "requestContentLength", 50 | "responseCode", 51 | "error", 52 | "responseContentLength" 53 | }; 54 | 55 | private static final SimpleDateFormat TIME_ONLY_FMT = new SimpleDateFormat("HH:mm:ss", Locale.US); 56 | 57 | private Long _id; 58 | @Index private Date requestDate; 59 | private Date responseDate; 60 | private Long tookMs; 61 | 62 | private String protocol; 63 | private String method; 64 | private String url; 65 | private String host; 66 | private String path; 67 | private String scheme; 68 | 69 | private Long requestContentLength; 70 | private String requestContentType; 71 | private String requestHeaders; 72 | private String requestBody; 73 | private boolean requestBodyIsPlainText = true; 74 | 75 | private Integer responseCode; 76 | private String responseMessage; 77 | private String error; 78 | 79 | private Long responseContentLength; 80 | private String responseContentType; 81 | private String responseHeaders; 82 | private String responseBody; 83 | private boolean responseBodyIsPlainText = true; 84 | 85 | public Long getId() { 86 | return _id; 87 | } 88 | 89 | public void setId(long id) { 90 | _id = id; 91 | } 92 | 93 | public Date getRequestDate() { 94 | return requestDate; 95 | } 96 | 97 | public void setRequestDate(Date requestDate) { 98 | this.requestDate = requestDate; 99 | } 100 | 101 | public Date getResponseDate() { 102 | return responseDate; 103 | } 104 | 105 | public void setResponseDate(Date responseDate) { 106 | this.responseDate = responseDate; 107 | } 108 | 109 | public String getError() { 110 | return error; 111 | } 112 | 113 | public void setError(String error) { 114 | this.error = error; 115 | } 116 | 117 | public String getMethod() { 118 | return method; 119 | } 120 | 121 | public void setMethod(String method) { 122 | this.method = method; 123 | } 124 | 125 | public String getProtocol() { 126 | return protocol; 127 | } 128 | 129 | public void setProtocol(String protocol) { 130 | this.protocol = protocol; 131 | } 132 | 133 | public String getRequestBody() { 134 | return requestBody; 135 | } 136 | 137 | public String getFormattedRequestBody() { 138 | return formatBody(requestBody, requestContentType); 139 | } 140 | 141 | public void setRequestBody(String requestBody) { 142 | this.requestBody = requestBody; 143 | } 144 | 145 | public boolean requestBodyIsPlainText() { 146 | return requestBodyIsPlainText; 147 | } 148 | 149 | public void setRequestBodyIsPlainText(boolean requestBodyIsPlainText) { 150 | this.requestBodyIsPlainText = requestBodyIsPlainText; 151 | } 152 | 153 | public Long getRequestContentLength() { 154 | return requestContentLength; 155 | } 156 | 157 | public void setRequestContentLength(Long requestContentLength) { 158 | this.requestContentLength = requestContentLength; 159 | } 160 | 161 | public String getRequestContentType() { 162 | return requestContentType; 163 | } 164 | 165 | public void setRequestContentType(String requestContentType) { 166 | this.requestContentType = requestContentType; 167 | } 168 | 169 | public String getResponseBody() { 170 | return responseBody; 171 | } 172 | 173 | public String getFormattedResponseBody() { 174 | return formatBody(responseBody, responseContentType); 175 | } 176 | 177 | public void setResponseBody(String responseBody) { 178 | this.responseBody = responseBody; 179 | } 180 | 181 | public boolean responseBodyIsPlainText() { 182 | return responseBodyIsPlainText; 183 | } 184 | 185 | public void setResponseBodyIsPlainText(boolean responseBodyIsPlainText) { 186 | this.responseBodyIsPlainText = responseBodyIsPlainText; 187 | } 188 | 189 | public Integer getResponseCode() { 190 | return responseCode; 191 | } 192 | 193 | public void setResponseCode(Integer responseCode) { 194 | this.responseCode = responseCode; 195 | } 196 | 197 | public Long getResponseContentLength() { 198 | return responseContentLength; 199 | } 200 | 201 | public void setResponseContentLength(Long responseContentLength) { 202 | this.responseContentLength = responseContentLength; 203 | } 204 | 205 | public String getResponseContentType() { 206 | return responseContentType; 207 | } 208 | 209 | public void setResponseContentType(String responseContentType) { 210 | this.responseContentType = responseContentType; 211 | } 212 | 213 | public String getResponseMessage() { 214 | return responseMessage; 215 | } 216 | 217 | public void setResponseMessage(String responseMessage) { 218 | this.responseMessage = responseMessage; 219 | } 220 | 221 | public Long getTookMs() { 222 | return tookMs; 223 | } 224 | 225 | public void setTookMs(Long tookMs) { 226 | this.tookMs = tookMs; 227 | } 228 | 229 | public String getUrl() { 230 | return url; 231 | } 232 | 233 | public void setUrl(String url) { 234 | this.url = url; 235 | Uri uri = Uri.parse(url); 236 | host = uri.getHost(); 237 | path = uri.getPath() + ((uri.getQuery() != null) ? "?" + uri.getQuery() : ""); 238 | scheme = uri.getScheme(); 239 | } 240 | 241 | public String getHost() { 242 | return host; 243 | } 244 | 245 | public String getPath() { 246 | return path; 247 | } 248 | 249 | public String getScheme() { 250 | return scheme; 251 | } 252 | 253 | public void setRequestHeaders(Headers headers) { 254 | setRequestHeaders(toHttpHeaderList(headers)); 255 | } 256 | 257 | public void setRequestHeaders(List headers) { 258 | requestHeaders = JsonConvertor.getInstance().toJson(headers); 259 | } 260 | 261 | public List getRequestHeaders() { 262 | return JsonConvertor.getInstance().fromJson(requestHeaders, 263 | new TypeToken>(){}.getType()); 264 | } 265 | 266 | public String getRequestHeadersString(boolean withMarkup) { 267 | return FormatUtils.formatHeaders(getRequestHeaders(), withMarkup); 268 | } 269 | 270 | public void setResponseHeaders(Headers headers) { 271 | setResponseHeaders(toHttpHeaderList(headers)); 272 | } 273 | 274 | public void setResponseHeaders(List headers) { 275 | responseHeaders = JsonConvertor.getInstance().toJson(headers); 276 | } 277 | 278 | public List getResponseHeaders() { 279 | return JsonConvertor.getInstance().fromJson(responseHeaders, 280 | new TypeToken>(){}.getType()); 281 | } 282 | 283 | public String getResponseHeadersString(boolean withMarkup) { 284 | return FormatUtils.formatHeaders(getResponseHeaders(), withMarkup); 285 | } 286 | 287 | public Status getStatus() { 288 | if (error != null) { 289 | return Status.Failed; 290 | } else if (responseCode == null) { 291 | return Status.Requested; 292 | } else { 293 | return Status.Complete; 294 | } 295 | } 296 | 297 | public String getRequestStartTimeString() { 298 | return (requestDate != null) ? TIME_ONLY_FMT.format(requestDate) : null; 299 | } 300 | 301 | public String getRequestDateString() { 302 | return (requestDate != null) ? requestDate.toString() : null; 303 | } 304 | 305 | public String getResponseDateString() { 306 | return (responseDate != null) ? responseDate.toString() : null; 307 | } 308 | 309 | public String getDurationString() { 310 | return (tookMs != null) ? + tookMs + " ms" : null; 311 | } 312 | 313 | public String getRequestSizeString() { 314 | return formatBytes((requestContentLength != null) ? requestContentLength : 0); 315 | } 316 | public String getResponseSizeString() { 317 | return (responseContentLength != null) ? formatBytes(responseContentLength) : null; 318 | } 319 | 320 | public String getTotalSizeString() { 321 | long reqBytes = (requestContentLength != null) ? requestContentLength : 0; 322 | long resBytes = (responseContentLength != null) ? responseContentLength : 0; 323 | return formatBytes(reqBytes + resBytes); 324 | } 325 | 326 | public String getResponseSummaryText() { 327 | switch (getStatus()) { 328 | case Failed: 329 | return error; 330 | case Requested: 331 | return null; 332 | default: 333 | return String.valueOf(responseCode) + " " + responseMessage; 334 | } 335 | } 336 | 337 | public String getNotificationText() { 338 | switch (getStatus()) { 339 | case Failed: 340 | return " ! ! ! " + path; 341 | case Requested: 342 | return " . . . " + path; 343 | default: 344 | return String.valueOf(responseCode) + " " + path; 345 | } 346 | } 347 | 348 | public boolean isSsl() { 349 | return scheme.toLowerCase().equals("https"); 350 | } 351 | 352 | private List toHttpHeaderList(Headers headers) { 353 | List httpHeaders = new ArrayList<>(); 354 | for (int i = 0, count = headers.size(); i < count; i++) { 355 | httpHeaders.add(new HttpHeader(headers.name(i), headers.value(i))); 356 | } 357 | return httpHeaders; 358 | } 359 | 360 | private String formatBody(String body, String contentType) { 361 | if (contentType != null && contentType.toLowerCase().contains("json")) { 362 | return FormatUtils.formatJson(body); 363 | } else if (contentType != null && contentType.toLowerCase().contains("xml")) { 364 | return FormatUtils.formatXml(body); 365 | } else { 366 | return body; 367 | } 368 | } 369 | 370 | private String formatBytes(long bytes) { 371 | return FormatUtils.formatByteCount(bytes, true); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/data/LocalCupboard.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.data; 17 | 18 | import nl.qbusict.cupboard.Cupboard; 19 | import nl.qbusict.cupboard.CupboardBuilder; 20 | 21 | public class LocalCupboard { 22 | 23 | private static Cupboard cupboard; 24 | 25 | static { 26 | getInstance().register(HttpTransaction.class); 27 | } 28 | 29 | public static Cupboard getInstance() { 30 | if (cupboard == null) { 31 | cupboard = new CupboardBuilder().build(); 32 | } 33 | return cupboard; 34 | } 35 | 36 | public static Cupboard getAnnotatedInstance() { 37 | return new CupboardBuilder(getInstance()) 38 | .useAnnotations() 39 | .build(); 40 | } 41 | 42 | private LocalCupboard() { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/ClearTransactionsService.java: -------------------------------------------------------------------------------- 1 | package com.readystatesoftware.chuck.internal.support; 2 | 3 | import android.app.IntentService; 4 | import android.content.Intent; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.readystatesoftware.chuck.internal.data.ChuckContentProvider; 8 | 9 | public class ClearTransactionsService extends IntentService { 10 | 11 | public ClearTransactionsService() { 12 | super("Chuck-ClearTransactionsService"); 13 | } 14 | 15 | @Override 16 | protected void onHandleIntent(@Nullable Intent intent) { 17 | getContentResolver().delete(ChuckContentProvider.TRANSACTION_URI, null, null); 18 | NotificationHelper.clearBuffer(); 19 | NotificationHelper notificationHelper = new NotificationHelper(this); 20 | notificationHelper.dismiss(); 21 | } 22 | } -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/FormatUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.support; 17 | 18 | import android.content.Context; 19 | import android.text.TextUtils; 20 | 21 | import com.readystatesoftware.chuck.R; 22 | import com.readystatesoftware.chuck.internal.data.HttpHeader; 23 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 24 | import com.google.gson.JsonElement; 25 | import com.google.gson.JsonParser; 26 | 27 | import org.xml.sax.InputSource; 28 | 29 | import java.io.ByteArrayInputStream; 30 | import java.io.ByteArrayOutputStream; 31 | import java.util.List; 32 | import java.util.Locale; 33 | 34 | import javax.xml.transform.OutputKeys; 35 | import javax.xml.transform.Source; 36 | import javax.xml.transform.Transformer; 37 | import javax.xml.transform.sax.SAXSource; 38 | import javax.xml.transform.sax.SAXTransformerFactory; 39 | import javax.xml.transform.stream.StreamResult; 40 | 41 | public class FormatUtils { 42 | 43 | public static String formatHeaders(List httpHeaders, boolean withMarkup) { 44 | String out = ""; 45 | if (httpHeaders != null) { 46 | for (HttpHeader header : httpHeaders) { 47 | out += ((withMarkup) ? "" : "") + header.getName() + ": " + ((withMarkup) ? "" : "") + 48 | header.getValue() + ((withMarkup) ? "
" : "\n"); 49 | } 50 | } 51 | return out; 52 | } 53 | 54 | public static String formatByteCount(long bytes, boolean si) { 55 | int unit = si ? 1000 : 1024; 56 | if (bytes < unit) return bytes + " B"; 57 | int exp = (int) (Math.log(bytes) / Math.log(unit)); 58 | String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); 59 | return String.format(Locale.US, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 60 | } 61 | 62 | public static String formatJson(String json) { 63 | try { 64 | JsonParser jp = new JsonParser(); 65 | JsonElement je = jp.parse(json); 66 | return JsonConvertor.getInstance().toJson(je); 67 | } catch (Exception e) { 68 | return json; 69 | } 70 | } 71 | 72 | public static String formatXml(String xml) { 73 | try { 74 | Transformer serializer = SAXTransformerFactory.newInstance().newTransformer(); 75 | serializer.setOutputProperty(OutputKeys.INDENT, "yes"); 76 | serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 77 | Source xmlSource = new SAXSource(new InputSource(new ByteArrayInputStream(xml.getBytes()))); 78 | StreamResult res = new StreamResult(new ByteArrayOutputStream()); 79 | serializer.transform(xmlSource, res); 80 | return new String(((ByteArrayOutputStream)res.getOutputStream()).toByteArray()); 81 | } catch (Exception e) { 82 | return xml; 83 | } 84 | } 85 | 86 | public static String getShareText(Context context, HttpTransaction transaction) { 87 | String text = ""; 88 | text += context.getString(R.string.chuck_url) + ": " + v(transaction.getUrl()) + "\n"; 89 | text += context.getString(R.string.chuck_method) + ": " + v(transaction.getMethod()) + "\n"; 90 | text += context.getString(R.string.chuck_protocol) + ": " + v(transaction.getProtocol()) + "\n"; 91 | text += context.getString(R.string.chuck_status) + ": " + v(transaction.getStatus().toString()) + "\n"; 92 | text += context.getString(R.string.chuck_response) + ": " + v(transaction.getResponseSummaryText()) + "\n"; 93 | text += context.getString(R.string.chuck_ssl) + ": " + v(context.getString(transaction.isSsl() ? R.string.chuck_yes : R.string.chuck_no)) + "\n"; 94 | text += "\n"; 95 | text += context.getString(R.string.chuck_request_time) + ": " + v(transaction.getRequestDateString()) + "\n"; 96 | text += context.getString(R.string.chuck_response_time) + ": " + v(transaction.getResponseDateString()) + "\n"; 97 | text += context.getString(R.string.chuck_duration) + ": " + v(transaction.getDurationString()) + "\n"; 98 | text += "\n"; 99 | text += context.getString(R.string.chuck_request_size) + ": " + v(transaction.getRequestSizeString()) + "\n"; 100 | text += context.getString(R.string.chuck_response_size) + ": " + v(transaction.getResponseSizeString()) + "\n"; 101 | text += context.getString(R.string.chuck_total_size) + ": " + v(transaction.getTotalSizeString()) + "\n"; 102 | text += "\n"; 103 | text += "---------- " + context.getString(R.string.chuck_request) + " ----------\n\n"; 104 | String headers = formatHeaders(transaction.getRequestHeaders(), false); 105 | if (!TextUtils.isEmpty(headers)) { 106 | text += headers + "\n"; 107 | } 108 | text += (transaction.requestBodyIsPlainText()) ? v(transaction.getFormattedRequestBody()) : 109 | context.getString(R.string.chuck_body_omitted); 110 | text += "\n\n"; 111 | text += "---------- " + context.getString(R.string.chuck_response) + " ----------\n\n"; 112 | headers = formatHeaders(transaction.getResponseHeaders(), false); 113 | if (!TextUtils.isEmpty(headers)) { 114 | text += headers + "\n"; 115 | } 116 | text += (transaction.responseBodyIsPlainText()) ? v(transaction.getFormattedResponseBody()) : 117 | context.getString(R.string.chuck_body_omitted); 118 | return text; 119 | } 120 | 121 | public static String getShareCurlCommand(HttpTransaction transaction) { 122 | boolean compressed = false; 123 | String curlCmd = "curl"; 124 | curlCmd += " -X " + transaction.getMethod(); 125 | List headers = transaction.getRequestHeaders(); 126 | for (int i = 0, count = headers.size(); i < count; i++) { 127 | String name = headers.get(i).getName(); 128 | String value = headers.get(i).getValue(); 129 | if ("Accept-Encoding".equalsIgnoreCase(name) && "gzip".equalsIgnoreCase(value)) { 130 | compressed = true; 131 | } 132 | curlCmd += " -H " + "\"" + name + ": " + value + "\""; 133 | } 134 | String requestBody = transaction.getRequestBody(); 135 | if (requestBody != null && requestBody.length() > 0) { 136 | // try to keep to a single line and use a subshell to preserve any line breaks 137 | curlCmd += " --data $'" + requestBody.replace("\n", "\\n") + "'"; 138 | } 139 | curlCmd += ((compressed) ? " --compressed " : " ") + transaction.getUrl(); 140 | return curlCmd; 141 | } 142 | 143 | private static String v(String string) { 144 | return (string != null) ? string : ""; 145 | } 146 | } -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/JsonConvertor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.support; 17 | 18 | import com.google.gson.FieldNamingPolicy; 19 | import com.google.gson.Gson; 20 | import com.google.gson.GsonBuilder; 21 | import com.google.gson.internal.bind.DateTypeAdapter; 22 | 23 | import java.util.Date; 24 | 25 | public class JsonConvertor { 26 | 27 | private static Gson gson = null; 28 | 29 | private JsonConvertor() { 30 | } 31 | 32 | public static Gson getInstance() { 33 | if (gson == null) { 34 | gson = new GsonBuilder() 35 | .setPrettyPrinting() 36 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 37 | .registerTypeAdapter(Date.class, new DateTypeAdapter()) 38 | .create(); 39 | } 40 | return gson; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/NotificationHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.support; 17 | 18 | import android.app.NotificationChannel; 19 | import android.app.NotificationManager; 20 | import android.app.PendingIntent; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.os.Build; 24 | import android.support.annotation.NonNull; 25 | import android.support.v4.app.NotificationCompat; 26 | import android.support.v4.content.ContextCompat; 27 | import android.util.LongSparseArray; 28 | 29 | import com.readystatesoftware.chuck.Chuck; 30 | import com.readystatesoftware.chuck.R; 31 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 32 | import com.readystatesoftware.chuck.internal.ui.BaseChuckActivity; 33 | 34 | import java.lang.reflect.Method; 35 | 36 | public class NotificationHelper { 37 | 38 | private static final String CHANNEL_ID = "chuck"; 39 | private static final int NOTIFICATION_ID = 1138; 40 | private static final int BUFFER_SIZE = 10; 41 | 42 | private static final LongSparseArray transactionBuffer = new LongSparseArray<>(); 43 | private static int transactionCount; 44 | 45 | private final Context context; 46 | private final NotificationManager notificationManager; 47 | private Method setChannelId; 48 | 49 | public static synchronized void clearBuffer() { 50 | transactionBuffer.clear(); 51 | transactionCount = 0; 52 | } 53 | 54 | private static synchronized void addToBuffer(HttpTransaction transaction) { 55 | if (transaction.getStatus() == HttpTransaction.Status.Requested) { 56 | transactionCount++; 57 | } 58 | transactionBuffer.put(transaction.getId(), transaction); 59 | if (transactionBuffer.size() > BUFFER_SIZE) { 60 | transactionBuffer.removeAt(0); 61 | } 62 | } 63 | 64 | public NotificationHelper(Context context) { 65 | this.context = context; 66 | notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 68 | notificationManager.createNotificationChannel( 69 | new NotificationChannel(CHANNEL_ID, 70 | context.getString(R.string.notification_category), NotificationManager.IMPORTANCE_LOW)); 71 | try { 72 | setChannelId = NotificationCompat.Builder.class.getMethod("setChannelId", String.class); 73 | } catch (Exception ignored) {} 74 | } 75 | } 76 | 77 | public synchronized void show(HttpTransaction transaction) { 78 | addToBuffer(transaction); 79 | if (!BaseChuckActivity.isInForeground()) { 80 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context) 81 | .setContentIntent(PendingIntent.getActivity(context, 0, Chuck.getLaunchIntent(context), 0)) 82 | .setLocalOnly(true) 83 | .setSmallIcon(R.drawable.chuck_ic_notification_white_24dp) 84 | .setColor(ContextCompat.getColor(context, R.color.chuck_colorPrimary)) 85 | .setContentTitle(context.getString(R.string.chuck_notification_title)); 86 | NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); 87 | if (setChannelId != null) { 88 | try { setChannelId.invoke(builder, CHANNEL_ID); } catch (Exception ignored) {} 89 | } 90 | int count = 0; 91 | for (int i = transactionBuffer.size() - 1; i >= 0; i--) { 92 | if (count < BUFFER_SIZE) { 93 | if (count == 0) { 94 | builder.setContentText(transactionBuffer.valueAt(i).getNotificationText()); 95 | } 96 | inboxStyle.addLine(transactionBuffer.valueAt(i).getNotificationText()); 97 | } 98 | count++; 99 | } 100 | builder.setAutoCancel(true); 101 | builder.setStyle(inboxStyle); 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 103 | builder.setSubText(String.valueOf(transactionCount)); 104 | } else { 105 | builder.setNumber(transactionCount); 106 | } 107 | builder.addAction(getClearAction()); 108 | notificationManager.notify(NOTIFICATION_ID, builder.build()); 109 | } 110 | } 111 | 112 | @NonNull 113 | private NotificationCompat.Action getClearAction() { 114 | CharSequence clearTitle = context.getString(R.string.chuck_clear); 115 | Intent deleteIntent = new Intent(context, ClearTransactionsService.class); 116 | PendingIntent intent = PendingIntent.getService(context, 11, deleteIntent, PendingIntent.FLAG_ONE_SHOT); 117 | return new NotificationCompat.Action(R.drawable.chuck_ic_delete_white_24dp, 118 | clearTitle, intent); 119 | } 120 | 121 | public void dismiss() { 122 | notificationManager.cancel(NOTIFICATION_ID); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/RetentionManager.java: -------------------------------------------------------------------------------- 1 | package com.readystatesoftware.chuck.internal.support; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import com.readystatesoftware.chuck.ChuckInterceptor; 8 | import com.readystatesoftware.chuck.internal.data.ChuckContentProvider; 9 | 10 | import java.util.Date; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class RetentionManager { 14 | 15 | private static final String LOG_TAG = "Chuck"; 16 | private static final String PREFS_NAME = "chuck_preferences"; 17 | private static final String KEY_LAST_CLEANUP = "last_cleanup"; 18 | 19 | private static long lastCleanup; 20 | 21 | private final Context context; 22 | private final long period; 23 | private final long cleanupFrequency; 24 | private final SharedPreferences prefs; 25 | 26 | public RetentionManager(Context context, ChuckInterceptor.Period retentionPeriod) { 27 | this.context = context; 28 | period = toMillis(retentionPeriod); 29 | prefs = context.getSharedPreferences(PREFS_NAME, 0); 30 | cleanupFrequency = (retentionPeriod == ChuckInterceptor.Period.ONE_HOUR) ? 31 | TimeUnit.MINUTES.toMillis(30) : TimeUnit.HOURS.toMillis(2); 32 | } 33 | 34 | public synchronized void doMaintenance() { 35 | if (period > 0) { 36 | long now = new Date().getTime(); 37 | if (isCleanupDue(now)) { 38 | Log.i(LOG_TAG, "Performing data retention maintenance..."); 39 | deleteSince(getThreshold(now)); 40 | updateLastCleanup(now); 41 | } 42 | } 43 | } 44 | 45 | private long getLastCleanup(long fallback) { 46 | if (lastCleanup == 0) { 47 | lastCleanup = prefs.getLong(KEY_LAST_CLEANUP, fallback); 48 | } 49 | return lastCleanup; 50 | } 51 | 52 | private void updateLastCleanup(long time) { 53 | lastCleanup = time; 54 | prefs.edit().putLong(KEY_LAST_CLEANUP, time).apply(); 55 | } 56 | 57 | private void deleteSince(long threshold) { 58 | int rows = context.getContentResolver().delete(ChuckContentProvider.TRANSACTION_URI, 59 | "requestDate <= ?", new String[] { String.valueOf(threshold) }); 60 | Log.i(LOG_TAG, rows + " transactions deleted"); 61 | } 62 | 63 | private boolean isCleanupDue(long now) { 64 | return (now - getLastCleanup(now)) > cleanupFrequency; 65 | } 66 | 67 | private long getThreshold(long now) { 68 | return (period == 0) ? now : now - period; 69 | } 70 | 71 | private long toMillis(ChuckInterceptor.Period period) { 72 | switch (period) { 73 | case ONE_HOUR: 74 | return TimeUnit.HOURS.toMillis(1); 75 | case ONE_DAY: 76 | return TimeUnit.DAYS.toMillis(1); 77 | case ONE_WEEK: 78 | return TimeUnit.DAYS.toMillis(7); 79 | default: 80 | return 0; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/SQLiteUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.support; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.net.Uri; 21 | import android.os.Environment; 22 | import android.widget.Toast; 23 | 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.io.FileOutputStream; 27 | import java.nio.channels.FileChannel; 28 | 29 | public class SQLiteUtils { 30 | 31 | public static void browseDatabase(Context context) { 32 | if (isIntentResolvable(context, getSQLiteDebuggerAppIntent("/"))) { 33 | String path = extractDatabase(context); 34 | if (path != null) { 35 | Intent intent = getSQLiteDebuggerAppIntent(path); 36 | context.startActivity(intent); 37 | } else { 38 | Toast.makeText(context, "Unable to extract database", Toast.LENGTH_SHORT).show(); 39 | } 40 | } else { 41 | Toast.makeText(context, "Unable to resolve a SQLite Intent", Toast.LENGTH_SHORT).show(); 42 | } 43 | } 44 | 45 | private static String extractDatabase(Context context) { 46 | try { 47 | File external = context.getExternalFilesDir(null); 48 | File data = Environment.getDataDirectory(); 49 | if (external != null && external.canWrite()) { 50 | String dataDBPath = "data/" + context.getPackageName() + "/databases/chuck.db"; 51 | String extractDBPath = "chuckdb.temp"; 52 | File dataDB = new File(data, dataDBPath); 53 | File extractDB = new File(external, extractDBPath); 54 | if (dataDB.exists()) { 55 | FileChannel in = new FileInputStream(dataDB).getChannel(); 56 | FileChannel out = new FileOutputStream(extractDB).getChannel(); 57 | out.transferFrom(in, 0, in.size()); 58 | in.close(); 59 | out.close(); 60 | return extractDB.getAbsolutePath(); 61 | } 62 | } 63 | } catch (Exception e) { 64 | e.printStackTrace(); 65 | } 66 | return null; 67 | } 68 | 69 | private static Intent getSQLiteDebuggerAppIntent(String path) { 70 | Intent intent = new Intent(Intent.ACTION_EDIT); 71 | intent.setData(Uri.parse("sqlite:" + path)); 72 | return intent; 73 | } 74 | 75 | private static boolean isIntentResolvable(Context context, Intent intent) { 76 | return context.getPackageManager().resolveActivity(intent, 0) != null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/support/SimpleOnPageChangedListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.support; 17 | 18 | import android.support.v4.view.ViewPager; 19 | 20 | public abstract class SimpleOnPageChangedListener implements ViewPager.OnPageChangeListener { 21 | @Override 22 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} 23 | @Override 24 | public abstract void onPageSelected(int position); 25 | @Override 26 | public void onPageScrollStateChanged(int state) {} 27 | } -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/BaseChuckActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.os.Bundle; 19 | import android.support.annotation.Nullable; 20 | import android.support.v7.app.AppCompatActivity; 21 | 22 | import com.readystatesoftware.chuck.internal.support.NotificationHelper; 23 | 24 | public abstract class BaseChuckActivity extends AppCompatActivity { 25 | 26 | private static boolean inForeground; 27 | 28 | private NotificationHelper notificationHelper; 29 | 30 | public static boolean isInForeground() { 31 | return inForeground; 32 | } 33 | 34 | @Override 35 | protected void onCreate(@Nullable Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | notificationHelper = new NotificationHelper(this); 38 | } 39 | 40 | @Override 41 | protected void onResume() { 42 | super.onResume(); 43 | inForeground = true; 44 | notificationHelper.dismiss(); 45 | } 46 | 47 | @Override 48 | protected void onPause() { 49 | super.onPause(); 50 | inForeground = false; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.content.pm.ApplicationInfo; 19 | import android.os.Bundle; 20 | import android.support.annotation.Nullable; 21 | import android.support.v7.widget.Toolbar; 22 | 23 | import com.readystatesoftware.chuck.R; 24 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 25 | 26 | public class MainActivity extends BaseChuckActivity implements TransactionListFragment.OnListFragmentInteractionListener { 27 | 28 | @Override 29 | protected void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.chuck_activity_main); 32 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 33 | setSupportActionBar(toolbar); 34 | toolbar.setSubtitle(getApplicationName()); 35 | if (savedInstanceState == null) { 36 | getSupportFragmentManager().beginTransaction() 37 | .add(R.id.container, TransactionListFragment.newInstance()) 38 | .commit(); 39 | } 40 | } 41 | 42 | @Override 43 | public void onListFragmentInteraction(HttpTransaction transaction) { 44 | TransactionActivity.start(this, transaction.getId()); 45 | } 46 | 47 | private String getApplicationName() { 48 | ApplicationInfo applicationInfo = getApplicationInfo(); 49 | int stringId = applicationInfo.labelRes; 50 | return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : getString(stringId); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.content.ContentUris; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.database.Cursor; 22 | import android.support.annotation.Nullable; 23 | import android.support.design.widget.TabLayout; 24 | import android.support.v4.app.Fragment; 25 | import android.support.v4.app.FragmentManager; 26 | import android.support.v4.app.FragmentPagerAdapter; 27 | import android.support.v4.app.LoaderManager; 28 | import android.support.v4.content.CursorLoader; 29 | import android.support.v4.content.Loader; 30 | import android.support.v4.view.ViewPager; 31 | import android.support.v7.app.ActionBar; 32 | import android.os.Bundle; 33 | import android.support.v7.widget.Toolbar; 34 | import android.view.Menu; 35 | import android.view.MenuInflater; 36 | import android.view.MenuItem; 37 | import android.widget.TextView; 38 | 39 | import com.readystatesoftware.chuck.R; 40 | import com.readystatesoftware.chuck.internal.data.ChuckContentProvider; 41 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 42 | import com.readystatesoftware.chuck.internal.data.LocalCupboard; 43 | import com.readystatesoftware.chuck.internal.support.FormatUtils; 44 | import com.readystatesoftware.chuck.internal.support.SimpleOnPageChangedListener; 45 | 46 | import java.util.ArrayList; 47 | import java.util.List; 48 | 49 | import static com.readystatesoftware.chuck.internal.ui.TransactionPayloadFragment.TYPE_REQUEST; 50 | import static com.readystatesoftware.chuck.internal.ui.TransactionPayloadFragment.TYPE_RESPONSE; 51 | 52 | public class TransactionActivity extends BaseChuckActivity implements LoaderManager.LoaderCallbacks { 53 | 54 | private static final String ARG_TRANSACTION_ID = "transaction_id"; 55 | 56 | private static int selectedTabPosition = 0; 57 | 58 | public static void start(Context context, long transactionId) { 59 | Intent intent = new Intent(context, TransactionActivity.class); 60 | intent.putExtra(ARG_TRANSACTION_ID, transactionId); 61 | context.startActivity(intent); 62 | } 63 | 64 | TextView title; 65 | Adapter adapter; 66 | 67 | private long transactionId; 68 | private HttpTransaction transaction; 69 | 70 | @Override 71 | protected void onCreate(@Nullable Bundle savedInstanceState) { 72 | super.onCreate(savedInstanceState); 73 | setContentView(R.layout.chuck_activity_transaction); 74 | 75 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 76 | setSupportActionBar(toolbar); 77 | title = (TextView) findViewById(R.id.toolbar_title); 78 | 79 | final ActionBar ab = getSupportActionBar(); 80 | ab.setDisplayHomeAsUpEnabled(true); 81 | 82 | ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager); 83 | if (viewPager != null) { 84 | setupViewPager(viewPager); 85 | } 86 | 87 | TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); 88 | tabLayout.setupWithViewPager(viewPager); 89 | 90 | transactionId = getIntent().getLongExtra(ARG_TRANSACTION_ID, 0); 91 | getSupportLoaderManager().initLoader(0, null, this); 92 | } 93 | 94 | @Override 95 | protected void onResume() { 96 | super.onResume(); 97 | getSupportLoaderManager().restartLoader(0, null, this); 98 | } 99 | 100 | @Override 101 | public boolean onCreateOptionsMenu(Menu menu) { 102 | MenuInflater inflater = getMenuInflater(); 103 | inflater.inflate(R.menu.chuck_transaction, menu); 104 | return super.onCreateOptionsMenu(menu); 105 | } 106 | 107 | @Override 108 | public boolean onOptionsItemSelected(MenuItem item) { 109 | if (item.getItemId() == R.id.share_text) { 110 | share(FormatUtils.getShareText(this, transaction)); 111 | return true; 112 | } else if (item.getItemId() == R.id.share_curl) { 113 | share(FormatUtils.getShareCurlCommand(transaction)); 114 | return true; 115 | } else { 116 | return super.onOptionsItemSelected(item); 117 | } 118 | } 119 | 120 | @Override 121 | public Loader onCreateLoader(int id, Bundle args) { 122 | CursorLoader loader = new CursorLoader(this); 123 | loader.setUri(ContentUris.withAppendedId(ChuckContentProvider.TRANSACTION_URI, transactionId)); 124 | return loader; 125 | } 126 | 127 | @Override 128 | public void onLoadFinished(Loader loader, Cursor data) { 129 | transaction = LocalCupboard.getInstance().withCursor(data).get(HttpTransaction.class); 130 | populateUI(); 131 | } 132 | 133 | @Override 134 | public void onLoaderReset(Loader loader) { 135 | } 136 | 137 | private void populateUI() { 138 | if (transaction != null) { 139 | title.setText(transaction.getMethod() + " " + transaction.getPath()); 140 | for (TransactionFragment fragment : adapter.fragments) { 141 | fragment.transactionUpdated(transaction); 142 | } 143 | } 144 | } 145 | 146 | private void setupViewPager(ViewPager viewPager) { 147 | adapter = new Adapter(getSupportFragmentManager()); 148 | adapter.addFragment(new TransactionOverviewFragment(), getString(R.string.chuck_overview)); 149 | adapter.addFragment(TransactionPayloadFragment.newInstance(TYPE_REQUEST), getString(R.string.chuck_request)); 150 | adapter.addFragment(TransactionPayloadFragment.newInstance(TYPE_RESPONSE), getString(R.string.chuck_response)); 151 | viewPager.setAdapter(adapter); 152 | viewPager.addOnPageChangeListener(new SimpleOnPageChangedListener() { 153 | @Override 154 | public void onPageSelected(int position) { 155 | selectedTabPosition = position; 156 | } 157 | }); 158 | viewPager.setCurrentItem(selectedTabPosition); 159 | } 160 | 161 | private void share(String content) { 162 | Intent sendIntent = new Intent(); 163 | sendIntent.setAction(Intent.ACTION_SEND); 164 | sendIntent.putExtra(Intent.EXTRA_TEXT, content); 165 | sendIntent.setType("text/plain"); 166 | startActivity(Intent.createChooser(sendIntent, null)); 167 | } 168 | 169 | static class Adapter extends FragmentPagerAdapter { 170 | final List fragments = new ArrayList<>(); 171 | private final List fragmentTitles = new ArrayList<>(); 172 | 173 | Adapter(FragmentManager fm) { 174 | super(fm); 175 | } 176 | 177 | void addFragment(TransactionFragment fragment, String title) { 178 | fragments.add(fragment); 179 | fragmentTitles.add(title); 180 | } 181 | 182 | @Override 183 | public Fragment getItem(int position) { 184 | return (Fragment) fragments.get(position); 185 | } 186 | 187 | @Override 188 | public int getCount() { 189 | return fragments.size(); 190 | } 191 | 192 | @Override 193 | public CharSequence getPageTitle(int position) { 194 | return fragmentTitles.get(position); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.content.Context; 19 | import android.database.Cursor; 20 | import android.support.v4.content.ContextCompat; 21 | import android.support.v4.widget.CursorAdapter; 22 | import android.support.v7.widget.RecyclerView; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.ImageView; 27 | import android.widget.TextView; 28 | 29 | import com.readystatesoftware.chuck.R; 30 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 31 | import com.readystatesoftware.chuck.internal.data.LocalCupboard; 32 | import com.readystatesoftware.chuck.internal.ui.TransactionListFragment.OnListFragmentInteractionListener; 33 | 34 | class TransactionAdapter extends RecyclerView.Adapter { 35 | 36 | private final Context context; 37 | private final OnListFragmentInteractionListener listener; 38 | private final CursorAdapter cursorAdapter; 39 | 40 | private final int colorDefault; 41 | private final int colorRequested; 42 | private final int colorError; 43 | private final int color500; 44 | private final int color400; 45 | private final int color300; 46 | 47 | TransactionAdapter(Context context, OnListFragmentInteractionListener listener) { 48 | this.listener = listener; 49 | this.context = context; 50 | colorDefault = ContextCompat.getColor(context, R.color.chuck_status_default); 51 | colorRequested = ContextCompat.getColor(context, R.color.chuck_status_requested); 52 | colorError = ContextCompat.getColor(context, R.color.chuck_status_error); 53 | color500 = ContextCompat.getColor(context, R.color.chuck_status_500); 54 | color400 = ContextCompat.getColor(context, R.color.chuck_status_400); 55 | color300 = ContextCompat.getColor(context, R.color.chuck_status_300); 56 | 57 | cursorAdapter = new CursorAdapter(TransactionAdapter.this.context, null, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER) { 58 | @Override 59 | public View newView(Context context, Cursor cursor, ViewGroup parent) { 60 | View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.chuck_list_item_transaction, parent, false); 61 | ViewHolder holder = new ViewHolder(itemView); 62 | itemView.setTag(holder); 63 | return itemView; 64 | } 65 | 66 | @Override 67 | public void bindView(View view, final Context context, Cursor cursor) { 68 | final HttpTransaction transaction = LocalCupboard.getInstance().withCursor(cursor).get(HttpTransaction.class); 69 | final ViewHolder holder = (ViewHolder) view.getTag(); 70 | holder.path.setText(transaction.getMethod() + " " + transaction.getPath()); 71 | holder.host.setText(transaction.getHost()); 72 | holder.start.setText(transaction.getRequestStartTimeString()); 73 | holder.ssl.setVisibility(transaction.isSsl() ? View.VISIBLE : View.GONE); 74 | if (transaction.getStatus() == HttpTransaction.Status.Complete) { 75 | holder.code.setText(String.valueOf(transaction.getResponseCode())); 76 | holder.duration.setText(transaction.getDurationString()); 77 | holder.size.setText(transaction.getTotalSizeString()); 78 | } else { 79 | holder.code.setText(null); 80 | holder.duration.setText(null); 81 | holder.size.setText(null); 82 | } 83 | if (transaction.getStatus() == HttpTransaction.Status.Failed) { 84 | holder.code.setText("!!!"); 85 | } 86 | setStatusColor(holder, transaction); 87 | holder.transaction = transaction; 88 | holder.view.setOnClickListener(new View.OnClickListener() { 89 | @Override 90 | public void onClick(View v) { 91 | if (null != TransactionAdapter.this.listener) { 92 | TransactionAdapter.this.listener.onListFragmentInteraction(holder.transaction); 93 | } 94 | } 95 | }); 96 | } 97 | 98 | private void setStatusColor(ViewHolder holder, HttpTransaction transaction) { 99 | int color; 100 | if (transaction.getStatus() == HttpTransaction.Status.Failed) { 101 | color = colorError; 102 | } else if (transaction.getStatus() == HttpTransaction.Status.Requested) { 103 | color = colorRequested; 104 | } else if (transaction.getResponseCode() >= 500) { 105 | color = color500; 106 | } else if (transaction.getResponseCode() >= 400) { 107 | color = color400; 108 | } else if (transaction.getResponseCode() >= 300) { 109 | color = color300; 110 | } else { 111 | color = colorDefault; 112 | } 113 | holder.code.setTextColor(color); 114 | holder.path.setTextColor(color); 115 | } 116 | }; 117 | } 118 | 119 | @Override 120 | public int getItemCount() { 121 | return cursorAdapter.getCount(); 122 | } 123 | 124 | @Override 125 | public void onBindViewHolder(ViewHolder holder, int position) { 126 | cursorAdapter.getCursor().moveToPosition(position); 127 | cursorAdapter.bindView(holder.itemView, context, cursorAdapter.getCursor()); 128 | } 129 | 130 | @Override 131 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 132 | View v = cursorAdapter.newView(context, cursorAdapter.getCursor(), parent); 133 | return new ViewHolder(v); 134 | } 135 | 136 | void swapCursor(Cursor newCursor) { 137 | cursorAdapter.swapCursor(newCursor); 138 | notifyDataSetChanged(); 139 | } 140 | 141 | class ViewHolder extends RecyclerView.ViewHolder { 142 | public final View view; 143 | public final TextView code; 144 | public final TextView path; 145 | public final TextView host; 146 | public final TextView start; 147 | public final TextView duration; 148 | public final TextView size; 149 | public final ImageView ssl; 150 | HttpTransaction transaction; 151 | 152 | ViewHolder(View view) { 153 | super(view); 154 | this.view = view; 155 | code = (TextView) view.findViewById(R.id.code); 156 | path = (TextView) view.findViewById(R.id.path); 157 | host = (TextView) view.findViewById(R.id.host); 158 | start = (TextView) view.findViewById(R.id.start); 159 | duration = (TextView) view.findViewById(R.id.duration); 160 | size = (TextView) view.findViewById(R.id.size); 161 | ssl = (ImageView) view.findViewById(R.id.ssl); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 19 | 20 | interface TransactionFragment { 21 | void transactionUpdated(HttpTransaction transaction); 22 | } -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionListFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.content.Context; 19 | import android.database.Cursor; 20 | import android.os.Bundle; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.app.Fragment; 23 | import android.support.v4.app.LoaderManager; 24 | import android.support.v4.content.CursorLoader; 25 | import android.support.v4.content.Loader; 26 | import android.support.v7.widget.DividerItemDecoration; 27 | import android.support.v7.widget.LinearLayoutManager; 28 | import android.support.v7.widget.RecyclerView; 29 | import android.support.v7.widget.SearchView; 30 | import android.text.TextUtils; 31 | import android.view.LayoutInflater; 32 | import android.view.Menu; 33 | import android.view.MenuInflater; 34 | import android.view.MenuItem; 35 | import android.view.View; 36 | import android.view.ViewGroup; 37 | 38 | import com.readystatesoftware.chuck.R; 39 | import com.readystatesoftware.chuck.internal.data.ChuckContentProvider; 40 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 41 | import com.readystatesoftware.chuck.internal.support.NotificationHelper; 42 | import com.readystatesoftware.chuck.internal.support.SQLiteUtils; 43 | 44 | public class TransactionListFragment extends Fragment implements 45 | SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks { 46 | 47 | private String currentFilter; 48 | private OnListFragmentInteractionListener listener; 49 | private TransactionAdapter adapter; 50 | 51 | public TransactionListFragment() {} 52 | 53 | public static TransactionListFragment newInstance() { 54 | return new TransactionListFragment(); 55 | } 56 | 57 | @Override 58 | public void onCreate(@Nullable Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setHasOptionsMenu(true); 61 | } 62 | 63 | @Override 64 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 65 | Bundle savedInstanceState) { 66 | View view = inflater.inflate(R.layout.chuck_fragment_transaction_list, container, false); 67 | if (view instanceof RecyclerView) { 68 | Context context = view.getContext(); 69 | RecyclerView recyclerView = (RecyclerView) view; 70 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 71 | recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), 72 | DividerItemDecoration.VERTICAL)); 73 | adapter = new TransactionAdapter(getContext(), listener); 74 | recyclerView.setAdapter(adapter); 75 | } 76 | return view; 77 | } 78 | 79 | @Override 80 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 81 | super.onActivityCreated(savedInstanceState); 82 | getLoaderManager().initLoader(0, null, this); 83 | } 84 | 85 | @Override 86 | public void onAttach(Context context) { 87 | super.onAttach(context); 88 | if (context instanceof OnListFragmentInteractionListener) { 89 | listener = (OnListFragmentInteractionListener) context; 90 | } else { 91 | throw new RuntimeException(context.toString() 92 | + " must implement OnListFragmentInteractionListener"); 93 | } 94 | } 95 | 96 | @Override 97 | public void onDetach() { 98 | super.onDetach(); 99 | listener = null; 100 | } 101 | 102 | @Override 103 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 104 | inflater.inflate(R.menu.chuck_main, menu); 105 | MenuItem searchMenuItem = menu.findItem(R.id.search); 106 | SearchView searchView = (SearchView) searchMenuItem.getActionView(); 107 | searchView.setOnQueryTextListener(this); 108 | searchView.setIconifiedByDefault(true); 109 | super.onCreateOptionsMenu(menu, inflater); 110 | } 111 | 112 | @Override 113 | public boolean onOptionsItemSelected(MenuItem item) { 114 | if (item.getItemId() == R.id.clear) { 115 | getContext().getContentResolver().delete(ChuckContentProvider.TRANSACTION_URI, null, null); 116 | NotificationHelper.clearBuffer(); 117 | return true; 118 | } else if (item.getItemId() == R.id.browse_sql) { 119 | SQLiteUtils.browseDatabase(getContext()); 120 | return true; 121 | } else { 122 | return super.onOptionsItemSelected(item); 123 | } 124 | } 125 | 126 | @Override 127 | public Loader onCreateLoader(int id, Bundle args) { 128 | CursorLoader loader = new CursorLoader(getContext()); 129 | loader.setUri(ChuckContentProvider.TRANSACTION_URI); 130 | if (!TextUtils.isEmpty(currentFilter)) { 131 | if (TextUtils.isDigitsOnly(currentFilter)) { 132 | loader.setSelection("responseCode LIKE ?"); 133 | loader.setSelectionArgs(new String[]{ currentFilter + "%" }); 134 | } else { 135 | loader.setSelection("path LIKE ?"); 136 | loader.setSelectionArgs(new String[]{ "%" + currentFilter + "%" }); 137 | } 138 | } 139 | loader.setProjection(HttpTransaction.PARTIAL_PROJECTION); 140 | loader.setSortOrder("requestDate DESC"); 141 | return loader; 142 | } 143 | 144 | @Override 145 | public void onLoadFinished(Loader loader, Cursor data) { 146 | adapter.swapCursor(data); 147 | } 148 | 149 | @Override 150 | public void onLoaderReset(Loader loader) { 151 | adapter.swapCursor(null); 152 | } 153 | 154 | @Override 155 | public boolean onQueryTextSubmit(String query) { 156 | return true; 157 | } 158 | 159 | @Override 160 | public boolean onQueryTextChange(String newText) { 161 | currentFilter = newText; 162 | getLoaderManager().restartLoader(0, null, this); 163 | return true; 164 | } 165 | 166 | public interface OnListFragmentInteractionListener { 167 | void onListFragmentInteraction(HttpTransaction item); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionOverviewFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.os.Bundle; 19 | import android.support.annotation.Nullable; 20 | import android.support.v4.app.Fragment; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import com.readystatesoftware.chuck.R; 27 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 28 | 29 | public class TransactionOverviewFragment extends Fragment implements TransactionFragment { 30 | 31 | TextView url; 32 | TextView method; 33 | TextView protocol; 34 | TextView status; 35 | TextView response; 36 | TextView ssl; 37 | TextView requestTime; 38 | TextView responseTime; 39 | TextView duration; 40 | TextView requestSize; 41 | TextView responseSize; 42 | TextView totalSize; 43 | 44 | private HttpTransaction transaction; 45 | 46 | public TransactionOverviewFragment() { 47 | } 48 | 49 | @Override 50 | public void onCreate(@Nullable Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | setRetainInstance(true); 53 | } 54 | 55 | @Override 56 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 57 | View view = inflater.inflate(R.layout.chuck_fragment_transaction_overview, container, false); 58 | url = (TextView) view.findViewById(R.id.url); 59 | method = (TextView) view.findViewById(R.id.method); 60 | protocol = (TextView) view.findViewById(R.id.protocol); 61 | status = (TextView) view.findViewById(R.id.status); 62 | response = (TextView) view.findViewById(R.id.response); 63 | ssl = (TextView) view.findViewById(R.id.ssl); 64 | requestTime = (TextView) view.findViewById(R.id.request_time); 65 | responseTime = (TextView) view.findViewById(R.id.response_time); 66 | duration = (TextView) view.findViewById(R.id.duration); 67 | requestSize = (TextView) view.findViewById(R.id.request_size); 68 | responseSize = (TextView) view.findViewById(R.id.response_size); 69 | totalSize = (TextView) view.findViewById(R.id.total_size); 70 | return view; 71 | } 72 | 73 | @Override 74 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 75 | super.onViewCreated(view, savedInstanceState); 76 | populateUI(); 77 | } 78 | 79 | @Override 80 | public void transactionUpdated(HttpTransaction transaction) { 81 | this.transaction = transaction; 82 | populateUI(); 83 | } 84 | 85 | private void populateUI() { 86 | if (isAdded() && transaction != null) { 87 | url.setText(transaction.getUrl()); 88 | method.setText(transaction.getMethod()); 89 | protocol.setText(transaction.getProtocol()); 90 | status.setText(transaction.getStatus().toString()); 91 | response.setText(transaction.getResponseSummaryText()); 92 | ssl.setText((transaction.isSsl() ? R.string.chuck_yes : R.string.chuck_no)); 93 | requestTime.setText(transaction.getRequestDateString()); 94 | responseTime.setText(transaction.getResponseDateString()); 95 | duration.setText(transaction.getDurationString()); 96 | requestSize.setText(transaction.getRequestSizeString()); 97 | responseSize.setText(transaction.getResponseSizeString()); 98 | totalSize.setText(transaction.getTotalSizeString()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /library/src/main/java/com/readystatesoftware/chuck/internal/ui/TransactionPayloadFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.internal.ui; 17 | 18 | import android.os.Bundle; 19 | import android.support.annotation.Nullable; 20 | import android.support.v4.app.Fragment; 21 | import android.text.Html; 22 | import android.text.TextUtils; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.TextView; 27 | 28 | import com.readystatesoftware.chuck.R; 29 | import com.readystatesoftware.chuck.internal.data.HttpTransaction; 30 | 31 | public class TransactionPayloadFragment extends Fragment implements TransactionFragment { 32 | 33 | public static final int TYPE_REQUEST = 0; 34 | public static final int TYPE_RESPONSE = 1; 35 | 36 | private static final String ARG_TYPE = "type"; 37 | 38 | TextView headers; 39 | TextView body; 40 | 41 | private int type; 42 | private HttpTransaction transaction; 43 | 44 | public TransactionPayloadFragment() { 45 | } 46 | 47 | public static TransactionPayloadFragment newInstance(int type) { 48 | TransactionPayloadFragment fragment = new TransactionPayloadFragment(); 49 | Bundle b = new Bundle(); 50 | b.putInt(ARG_TYPE, type); 51 | fragment.setArguments(b); 52 | return fragment; 53 | } 54 | 55 | @Override 56 | public void onCreate(@Nullable Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | type = getArguments().getInt(ARG_TYPE); 59 | setRetainInstance(true); 60 | } 61 | 62 | @Override 63 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 64 | @Nullable Bundle savedInstanceState) { 65 | View view = inflater.inflate(R.layout.chuck_fragment_transaction_payload, container, false); 66 | headers = (TextView) view.findViewById(R.id.headers); 67 | body = (TextView) view.findViewById(R.id.body); 68 | return view; 69 | } 70 | 71 | @Override 72 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 73 | super.onViewCreated(view, savedInstanceState); 74 | populateUI(); 75 | } 76 | 77 | @Override 78 | public void transactionUpdated(HttpTransaction transaction) { 79 | this.transaction = transaction; 80 | populateUI(); 81 | } 82 | 83 | private void populateUI() { 84 | if (isAdded() && transaction != null) { 85 | switch (type) { 86 | case TYPE_REQUEST: 87 | setText(transaction.getRequestHeadersString(true), 88 | transaction.getFormattedRequestBody(), transaction.requestBodyIsPlainText()); 89 | break; 90 | case TYPE_RESPONSE: 91 | setText(transaction.getResponseHeadersString(true), 92 | transaction.getFormattedResponseBody(), transaction.responseBodyIsPlainText()); 93 | break; 94 | } 95 | } 96 | } 97 | 98 | private void setText(String headersString, String bodyString, boolean isPlainText) { 99 | headers.setVisibility((TextUtils.isEmpty(headersString) ? View.GONE : View.VISIBLE)); 100 | headers.setText(Html.fromHtml(headersString)); 101 | if (!isPlainText) { 102 | body.setText(getString(R.string.chuck_body_omitted)); 103 | } else { 104 | body.setText(bodyString); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /library/src/main/res/drawable/chuck_ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chuck_ic_https_grey_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chuck_ic_notification_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chuck_ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/chuck_ic_share_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 29 | 30 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_activity_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 30 | 31 | 38 | 39 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 60 | 61 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_fragment_transaction_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 27 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_fragment_transaction_overview.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 29 | 30 | 33 | 34 | 39 | 40 | 46 | 47 | 48 | 49 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 71 | 72 | 77 | 78 | 84 | 85 | 86 | 87 | 90 | 91 | 96 | 97 | 103 | 104 | 105 | 106 | 109 | 110 | 115 | 116 | 122 | 123 | 124 | 125 | 128 | 129 | 134 | 135 | 141 | 142 | 143 | 144 | 148 | 149 | 154 | 155 | 161 | 162 | 163 | 164 | 167 | 168 | 173 | 174 | 180 | 181 | 182 | 183 | 186 | 187 | 192 | 193 | 199 | 200 | 201 | 202 | 206 | 207 | 212 | 213 | 219 | 220 | 221 | 222 | 225 | 226 | 231 | 232 | 238 | 239 | 240 | 241 | 244 | 245 | 250 | 251 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_fragment_transaction_payload.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /library/src/main/res/layout/chuck_list_item_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 33 | 34 | 43 | 44 | 53 | 54 | 64 | 65 | 72 | 73 | 79 | 80 | 87 | 88 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /library/src/main/res/menu/chuck_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 24 | 28 | 32 | -------------------------------------------------------------------------------- /library/src/main/res/menu/chuck_transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /library/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | -------------------------------------------------------------------------------- /library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | #00BCD4 19 | #0097A7 20 | #FF9800 21 | 22 | #212121 23 | #9E9E9E 24 | #F44336 25 | #B71C1C 26 | #FF9800 27 | #0D47A1 28 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Chuck 19 | Recording HTTP activity 20 | Clear 21 | Browse SQLite database 22 | Overview 23 | Request 24 | Response 25 | URL 26 | Method 27 | Protocol 28 | Status 29 | SSL 30 | Request time 31 | Response time 32 | Duration 33 | Request size 34 | Response size 35 | Total size 36 | Yes 37 | No 38 | Share 39 | Share as text 40 | Share as curl command 41 | (encoded or binary body omitted) 42 | Search 43 | \n\n--- Unexpected end of content --- 44 | \n\n--- Content truncated --- 45 | Chuck HTTP notifications 46 | 47 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 27 | 28 | 31 | 32 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | applicationId "com.readystatesoftware.chuck.sample" 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | debug { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | debugCompile project(':library') 29 | releaseCompile project(':library-no-op') 30 | compile "com.android.support:design:26.0.0" 31 | compile "com.android.support:appcompat-v7:26.0.0" 32 | compile "com.squareup.okhttp3:logging-interceptor:$okhttp3Version" 33 | compile "com.squareup.retrofit2:retrofit:2.2.0" 34 | compile "com.squareup.retrofit2:converter-gson:2.2.0" 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /sample/src/main/java/com/readystatesoftware/chuck/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.sample; 17 | 18 | import android.content.Context; 19 | import android.os.Bundle; 20 | import android.support.annotation.Nullable; 21 | import android.support.v7.app.AppCompatActivity; 22 | import android.view.View; 23 | 24 | import com.readystatesoftware.chuck.Chuck; 25 | import com.readystatesoftware.chuck.ChuckInterceptor; 26 | 27 | import okhttp3.OkHttpClient; 28 | import okhttp3.logging.HttpLoggingInterceptor; 29 | import retrofit2.Call; 30 | import retrofit2.Callback; 31 | import retrofit2.Response; 32 | 33 | public class MainActivity extends AppCompatActivity { 34 | 35 | @Override 36 | protected void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_main); 39 | findViewById(R.id.do_http).setOnClickListener(new View.OnClickListener() { 40 | @Override 41 | public void onClick(View view) { 42 | doHttpActivity(); 43 | } 44 | }); 45 | findViewById(R.id.launch_chuck_directly).setOnClickListener(new View.OnClickListener() { 46 | @Override 47 | public void onClick(View view) { 48 | launchChuckDirectly(); 49 | } 50 | }); 51 | } 52 | 53 | private OkHttpClient getClient(Context context) { 54 | return new OkHttpClient.Builder() 55 | // Add a ChuckInterceptor instance to your OkHttp client 56 | .addInterceptor(new ChuckInterceptor(context)) 57 | .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) 58 | .build(); 59 | } 60 | 61 | private void launchChuckDirectly() { 62 | // Optionally launch Chuck directly from your own app UI 63 | startActivity(Chuck.getLaunchIntent(this)); 64 | } 65 | 66 | private void doHttpActivity() { 67 | SampleApiService.HttpbinApi api = SampleApiService.getInstance(getClient(this)); 68 | Callback cb = new Callback() { 69 | @Override public void onResponse(Call call, Response response) {} 70 | @Override public void onFailure(Call call, Throwable t) { t.printStackTrace(); } 71 | }; 72 | api.get().enqueue(cb); 73 | api.post(new SampleApiService.Data("posted")).enqueue(cb); 74 | api.patch(new SampleApiService.Data("patched")).enqueue(cb); 75 | api.put(new SampleApiService.Data("put")).enqueue(cb); 76 | api.delete().enqueue(cb); 77 | api.status(201).enqueue(cb); 78 | api.status(401).enqueue(cb); 79 | api.status(500).enqueue(cb); 80 | api.delay(9).enqueue(cb); 81 | api.delay(15).enqueue(cb); 82 | api.redirectTo("https://http2.akamai.com").enqueue(cb); 83 | api.redirect(3).enqueue(cb); 84 | api.redirectRelative(2).enqueue(cb); 85 | api.redirectAbsolute(4).enqueue(cb); 86 | api.stream(500).enqueue(cb); 87 | api.streamBytes(2048).enqueue(cb); 88 | api.image("image/png").enqueue(cb); 89 | api.gzip().enqueue(cb); 90 | api.xml().enqueue(cb); 91 | api.utf8().enqueue(cb); 92 | api.deflate().enqueue(cb); 93 | api.cookieSet("v").enqueue(cb); 94 | api.basicAuth("me", "pass").enqueue(cb); 95 | api.drip(512, 5, 1, 200).enqueue(cb); 96 | api.deny().enqueue(cb); 97 | api.cache("Mon").enqueue(cb); 98 | api.cache(30).enqueue(cb); 99 | } 100 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/readystatesoftware/chuck/sample/SampleApiService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jeff Gilfelt. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.readystatesoftware.chuck.sample; 17 | 18 | import okhttp3.OkHttpClient; 19 | import retrofit2.Call; 20 | import retrofit2.Retrofit; 21 | import retrofit2.converter.gson.GsonConverterFactory; 22 | import retrofit2.http.Body; 23 | import retrofit2.http.DELETE; 24 | import retrofit2.http.GET; 25 | import retrofit2.http.Header; 26 | import retrofit2.http.PATCH; 27 | import retrofit2.http.POST; 28 | import retrofit2.http.PUT; 29 | import retrofit2.http.Path; 30 | import retrofit2.http.Query; 31 | 32 | class SampleApiService { 33 | 34 | static HttpbinApi getInstance(OkHttpClient client) { 35 | Retrofit retrofit = new Retrofit.Builder() 36 | .baseUrl("https://httpbin.org") 37 | .addConverterFactory(GsonConverterFactory.create()) 38 | .client(client) 39 | .build(); 40 | return retrofit.create(HttpbinApi.class); 41 | } 42 | 43 | static class Data { 44 | final String thing; 45 | Data(String thing) { 46 | this.thing = thing; 47 | } 48 | } 49 | 50 | interface HttpbinApi { 51 | @GET("/get") 52 | Call get(); 53 | @POST("/post") 54 | Call post(@Body Data body); 55 | @PATCH("/patch") 56 | Call patch(@Body Data body); 57 | @PUT("/put") 58 | Call put(@Body Data body); 59 | @DELETE("/delete") 60 | Call delete(); 61 | @GET("/status/{code}") 62 | Call status(@Path("code") int code); 63 | @GET("/stream/{lines}") 64 | Call stream(@Path("lines") int lines); 65 | @GET("/stream-bytes/{bytes}") 66 | Call streamBytes(@Path("bytes") int bytes); 67 | @GET("/delay/{seconds}") 68 | Call delay(@Path("seconds") int seconds); 69 | @GET("/redirect-to") 70 | Call redirectTo(@Query("url") String url); 71 | @GET("/redirect/{times}") 72 | Call redirect(@Path("times") int times); 73 | @GET("/relative-redirect/{times}") 74 | Call redirectRelative(@Path("times") int times); 75 | @GET("/absolute-redirect/{times}") 76 | Call redirectAbsolute(@Path("times") int times); 77 | @GET("/image") 78 | Call image(@Header("Accept") String accept); 79 | @GET("/gzip") 80 | Call gzip(); 81 | @GET("/xml") 82 | Call xml(); 83 | @GET("/encoding/utf8") 84 | Call utf8(); 85 | @GET("/deflate") 86 | Call deflate(); 87 | @GET("/cookies/set") 88 | Call cookieSet(@Query("k1") String value); 89 | @GET("/basic-auth/{user}/{passwd}") 90 | Call basicAuth(@Path("user") String user, @Path("passwd") String passwd); 91 | @GET("/drip") 92 | Call drip(@Query("numbytes") int bytes, @Query("duration") int seconds, @Query("delay") int delay, @Query("code") int code); 93 | @GET("/deny") 94 | Call deny(); 95 | @GET("/cache") 96 | Call cache(@Header("If-Modified-Since") String ifModifiedSince); 97 | @GET("/cache/{seconds}") 98 | Call cache(@Path("seconds") int seconds); 99 | } 100 | } -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 27 | 28 |