15
15
*/
16
16
package com.chuckerteam.chucker.internal.ui.transaction
17
17
18
+ import android.annotation.SuppressLint
19
+ import android.app.Activity
18
20
import android.content.Context
21
+ import android.content.Intent
19
22
import android.graphics.Bitmap
20
23
import android.graphics.Color
24
+ import android.net.Uri
21
25
import android.os.AsyncTask
26
+ import android.os.Build
22
27
import android.os.Bundle
23
28
import android.view.LayoutInflater
24
29
import android.view.Menu
@@ -27,6 +32,8 @@ import android.view.View
27
32
import android.view.ViewGroup
28
33
import android.widget.ImageView
29
34
import android.widget.TextView
35
+ import android.widget.Toast
36
+ import androidx.annotation.RequiresApi
30
37
import androidx.appcompat.widget.SearchView
31
38
import androidx.core.content.ContextCompat
32
39
import androidx.core.text.HtmlCompat
@@ -36,6 +43,11 @@ import androidx.lifecycle.ViewModelProviders
36
43
import com.chuckerteam.chucker.R
37
44
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
38
45
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
39
51
40
52
internal class TransactionPayloadFragment :
41
53
Fragment (), SearchView .OnQueryTextListener {
@@ -51,6 +63,7 @@ internal class TransactionPayloadFragment :
51
63
private lateinit var viewModel: TransactionViewModel
52
64
private var originalBody: String? = null
53
65
private var uiLoaderTask: UiLoaderTask ? = null
66
+ private var fileSaverTask: FileSaverTask ? = null
54
67
55
68
override fun onCreate (savedInstanceState : Bundle ? ) {
56
69
super .onCreate(savedInstanceState)
@@ -83,15 +96,38 @@ internal class TransactionPayloadFragment :
83
96
override fun onDestroyView () {
84
97
super .onDestroyView()
85
98
uiLoaderTask?.cancel(true )
99
+ fileSaverTask?.cancel(true )
86
100
}
87
101
102
+ @SuppressLint(" NewApi" )
88
103
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
+ }
95
131
}
96
132
97
133
super .onCreateOptionsMenu(menu, inflater)
@@ -104,6 +140,7 @@ internal class TransactionPayloadFragment :
104
140
}
105
141
106
142
private fun setBody (headersString : String , bodyString : String? , isPlainText : Boolean , image : Bitmap ? ) {
143
+ uiLoaderTask = null
107
144
headers.visibility = if (headersString.isEmpty()) View .GONE else View .VISIBLE
108
145
headers.text = HtmlCompat .fromHtml(headersString, HtmlCompat .FROM_HTML_MODE_LEGACY )
109
146
val isImageData = image != null
@@ -122,6 +159,38 @@ internal class TransactionPayloadFragment :
122
159
activity?.invalidateOptionsMenu()
123
160
}
124
161
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
+
125
194
override fun onQueryTextSubmit (query : String ): Boolean = false
126
195
127
196
override fun onQueryTextChange (newText : String ): Boolean {
@@ -158,6 +227,50 @@ internal class TransactionPayloadFragment :
158
227
}
159
228
}
160
229
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
+
161
274
private data class UiPayload (
162
275
val headersString : String ,
163
276
val bodyString : String? ,
@@ -171,6 +284,8 @@ internal class TransactionPayloadFragment :
171
284
const val TYPE_REQUEST = 0
172
285
const val TYPE_RESPONSE = 1
173
286
287
+ const val DEFAULT_FILE_PREFIX = " chucker-export-"
288
+
174
289
fun newInstance (type : Int ): TransactionPayloadFragment =
175
290
TransactionPayloadFragment ().apply {
176
291
arguments = Bundle ().apply {
0 commit comments