Skip to content

Commit 55623e3

Browse files
yoavstcortinico
authored andcommitted
Add an option to save the request or response body (#138)
* Add an option to save the request or response body * Hide the save button on API < 19 * Hide save icon if body is empty Co-authored-by: Nicola Corti <corti.nico@gmail.com>
1 parent 57d9748 commit 55623e3

File tree

7 files changed

+155
-7
lines changed

7 files changed

+155
-7
lines changed

detekt-config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ build:
88

99
complexity:
1010
active: true
11+
ComplexMethod:
12+
active: true
13+
threshold: 16
1114
ComplexCondition:
1215
active: true
1316
threshold: 5

library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ChuckerInterceptor @JvmOverloads constructor(
4545
}
4646

4747
@Throws(IOException::class)
48-
@Suppress("LongMethod", "ComplexMethod")
48+
@Suppress("LongMethod")
4949
override fun intercept(chain: Interceptor.Chain): Response {
5050
val request = chain.request()
5151
val requestBody = request.body()

library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package com.chuckerteam.chucker.internal.ui.transaction
1717

1818
import android.os.Bundle
1919
import android.view.LayoutInflater
20+
import android.view.Menu
21+
import android.view.MenuInflater
2022
import android.view.View
2123
import android.view.ViewGroup
2224
import android.widget.TextView
@@ -43,6 +45,7 @@ class TransactionOverviewFragment : Fragment() {
4345

4446
override fun onCreate(savedInstanceState: Bundle?) {
4547
super.onCreate(savedInstanceState)
48+
setHasOptionsMenu(true)
4649
viewModel = ViewModelProviders.of(requireActivity())[TransactionViewModel::class.java]
4750
}
4851

@@ -67,6 +70,13 @@ class TransactionOverviewFragment : Fragment() {
6770
totalSize = it.findViewById(R.id.total_size)
6871
}
6972

73+
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
74+
val saveMenuItem = menu.findItem(R.id.save_body)
75+
saveMenuItem.isVisible = false
76+
77+
super.onCreateOptionsMenu(menu, inflater)
78+
}
79+
7080
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
7181
super.onViewCreated(view, savedInstanceState)
7282
viewModel.transaction.observe(

library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt

+121-6
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
*/
1616
package com.chuckerteam.chucker.internal.ui.transaction
1717

18+
import android.annotation.SuppressLint
19+
import android.app.Activity
1820
import android.content.Context
21+
import android.content.Intent
1922
import android.graphics.Bitmap
2023
import android.graphics.Color
24+
import android.net.Uri
2125
import android.os.AsyncTask
26+
import android.os.Build
2227
import android.os.Bundle
2328
import android.view.LayoutInflater
2429
import android.view.Menu
@@ -27,6 +32,8 @@ import android.view.View
2732
import android.view.ViewGroup
2833
import android.widget.ImageView
2934
import android.widget.TextView
35+
import android.widget.Toast
36+
import androidx.annotation.RequiresApi
3037
import androidx.appcompat.widget.SearchView
3138
import androidx.core.content.ContextCompat
3239
import androidx.core.text.HtmlCompat
@@ -36,6 +43,11 @@ import androidx.lifecycle.ViewModelProviders
3643
import com.chuckerteam.chucker.R
3744
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
3845
import com.chuckerteam.chucker.internal.support.highlightWithDefinedColors
46+
import java.io.FileNotFoundException
47+
import java.io.FileOutputStream
48+
import java.io.IOException
49+
50+
private const val GET_FILE_FOR_SAVING_REQUEST_CODE: Int = 43
3951

4052
internal class TransactionPayloadFragment :
4153
Fragment(), SearchView.OnQueryTextListener {
@@ -51,6 +63,7 @@ internal class TransactionPayloadFragment :
5163
private lateinit var viewModel: TransactionViewModel
5264
private var originalBody: String? = null
5365
private var uiLoaderTask: UiLoaderTask? = null
66+
private var fileSaverTask: FileSaverTask? = null
5467

5568
override fun onCreate(savedInstanceState: Bundle?) {
5669
super.onCreate(savedInstanceState)
@@ -83,15 +96,38 @@ internal class TransactionPayloadFragment :
8396
override fun onDestroyView() {
8497
super.onDestroyView()
8598
uiLoaderTask?.cancel(true)
99+
fileSaverTask?.cancel(true)
86100
}
87101

102+
@SuppressLint("NewApi")
88103
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
89-
if ((type == TYPE_RESPONSE || type == TYPE_REQUEST) && body.text.isNotEmpty()) {
90-
val searchMenuItem = menu.findItem(R.id.search)
91-
searchMenuItem.isVisible = true
92-
val searchView = searchMenuItem.actionView as SearchView
93-
searchView.setOnQueryTextListener(this)
94-
searchView.setIconifiedByDefault(true)
104+
if ((type == TYPE_RESPONSE || type == TYPE_REQUEST)) {
105+
if (body.text.isNotEmpty()) {
106+
val searchMenuItem = menu.findItem(R.id.search)
107+
searchMenuItem.isVisible = true
108+
val searchView = searchMenuItem.actionView as SearchView
109+
searchView.setOnQueryTextListener(this)
110+
searchView.setIconifiedByDefault(true)
111+
}
112+
113+
val transaction = viewModel.transaction.value
114+
val showSaveMenuItem = when {
115+
// SAF is not available on pre-Kit Kat so let's hide the icon.
116+
(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) -> false
117+
(type == TYPE_REQUEST && 0L == (transaction?.requestContentLength ?: 0L)) -> false
118+
(type == TYPE_RESPONSE && 0L == (transaction?.responseContentLength ?: 0L)) -> false
119+
else -> true
120+
}
121+
122+
if (showSaveMenuItem) {
123+
menu.findItem(R.id.save_body).apply {
124+
isVisible = true
125+
setOnMenuItemClickListener {
126+
viewBodyExternally()
127+
true
128+
}
129+
}
130+
}
95131
}
96132

97133
super.onCreateOptionsMenu(menu, inflater)
@@ -104,6 +140,7 @@ internal class TransactionPayloadFragment :
104140
}
105141

106142
private fun setBody(headersString: String, bodyString: String?, isPlainText: Boolean, image: Bitmap?) {
143+
uiLoaderTask = null
107144
headers.visibility = if (headersString.isEmpty()) View.GONE else View.VISIBLE
108145
headers.text = HtmlCompat.fromHtml(headersString, HtmlCompat.FROM_HTML_MODE_LEGACY)
109146
val isImageData = image != null
@@ -122,6 +159,38 @@ internal class TransactionPayloadFragment :
122159
activity?.invalidateOptionsMenu()
123160
}
124161

162+
@RequiresApi(Build.VERSION_CODES.KITKAT)
163+
@SuppressLint("DefaultLocale")
164+
private fun viewBodyExternally() {
165+
viewModel.transaction.value?.let {
166+
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
167+
addCategory(Intent.CATEGORY_OPENABLE)
168+
putExtra(Intent.EXTRA_TITLE, "$DEFAULT_FILE_PREFIX${System.currentTimeMillis()}")
169+
type = "*/*"
170+
}
171+
if (intent.resolveActivity(requireActivity().packageManager) != null) {
172+
startActivityForResult(intent, GET_FILE_FOR_SAVING_REQUEST_CODE)
173+
} else {
174+
Toast.makeText(
175+
requireContext(), R.string.chucker_save_failed_to_open_document,
176+
Toast.LENGTH_SHORT
177+
).show()
178+
}
179+
}
180+
}
181+
182+
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
183+
if (requestCode == GET_FILE_FOR_SAVING_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
184+
val uri = resultData?.data
185+
val transaction = viewModel.transaction.value
186+
if (uri != null && transaction != null) {
187+
fileSaverTask = FileSaverTask(this).apply {
188+
execute(Triple(type, uri, transaction))
189+
}
190+
}
191+
}
192+
}
193+
125194
override fun onQueryTextSubmit(query: String): Boolean = false
126195

127196
override fun onQueryTextChange(newText: String): Boolean {
@@ -158,6 +227,50 @@ internal class TransactionPayloadFragment :
158227
}
159228
}
160229

230+
private class FileSaverTask(val fragment: TransactionPayloadFragment) :
231+
AsyncTask<Triple<Int, Uri, HttpTransaction>, Unit, Boolean>() {
232+
233+
@Suppress("NestedBlockDepth")
234+
override fun doInBackground(vararg params: Triple<Int, Uri, HttpTransaction>): Boolean {
235+
val (type, uri, transaction) = params[0]
236+
try {
237+
val context = fragment.context ?: return false
238+
context.contentResolver.openFileDescriptor(uri, "w")?.use {
239+
FileOutputStream(it.fileDescriptor).use { fos ->
240+
when {
241+
type == TYPE_REQUEST -> {
242+
transaction.requestBody?.byteInputStream()?.copyTo(fos)
243+
}
244+
transaction.responseBody != null -> {
245+
transaction.responseBody?.byteInputStream()?.copyTo(fos)
246+
}
247+
else -> {
248+
fos.write(transaction.responseImageData)
249+
}
250+
}
251+
}
252+
}
253+
} catch (e: FileNotFoundException) {
254+
e.printStackTrace()
255+
return false
256+
} catch (e: IOException) {
257+
e.printStackTrace()
258+
return false
259+
}
260+
return true
261+
}
262+
263+
override fun onPostExecute(isSuccessful: Boolean) {
264+
fragment.fileSaverTask = null
265+
val toastMessageId = if (isSuccessful) {
266+
R.string.chucker_file_saved
267+
} else {
268+
R.string.chucker_file_not_saved
269+
}
270+
Toast.makeText(fragment.context, toastMessageId, Toast.LENGTH_SHORT).show()
271+
}
272+
}
273+
161274
private data class UiPayload(
162275
val headersString: String,
163276
val bodyString: String?,
@@ -171,6 +284,8 @@ internal class TransactionPayloadFragment :
171284
const val TYPE_REQUEST = 0
172285
const val TYPE_RESPONSE = 1
173286

287+
const val DEFAULT_FILE_PREFIX = "chucker-export-"
288+
174289
fun newInstance(type: Int): TransactionPayloadFragment =
175290
TransactionPayloadFragment().apply {
176291
arguments = Bundle().apply {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="#FFFFFFFF"
8+
android:pathData="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
9+
</vector>

library/src/main/res/menu/chucker_transaction.xml

+7
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@
3838
</group>
3939
</menu>
4040
</item>
41+
<item
42+
android:icon="@drawable/chucker_ic_save_white_24dp"
43+
android:title="@string/chucker_save"
44+
android:id="@+id/save_body"
45+
android:visible="false"
46+
app:showAsAction="always">
47+
</item>
4148
</menu>

library/src/main/res/values/strings.xml

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<string name="chucker_share">Share</string>
3939
<string name="chucker_share_as_text">Share as text</string>
4040
<string name="chucker_share_as_curl">Share as curl command</string>
41+
<string name="chucker_save">Save body to file</string>
42+
<string name="chucker_save_failed_to_open_document">Failed to open file chooser</string>
43+
<string name="chucker_file_saved">File was saved successfully!</string>
44+
<string name="chucker_file_not_saved">Failed to save file</string>
4145
<string name="chucker_body_omitted">(encoded or binary body omitted)</string>
4246
<string name="chucker_search">Search</string>
4347
<string name="chucker_body_unexpected_eof">\n\n--- Unexpected end of content ---</string>

0 commit comments

Comments
 (0)