@@ -17,6 +17,7 @@ import androidx.annotation.RequiresApi
17
17
import androidx.annotation.StringRes
18
18
import androidx.appcompat.widget.SearchView
19
19
import androidx.core.content.ContextCompat
20
+ import androidx.core.view.MenuCompat
20
21
import androidx.core.view.isVisible
21
22
import androidx.lifecycle.lifecycleScope
22
23
import androidx.recyclerview.widget.DividerItemDecoration
@@ -25,19 +26,25 @@ import com.chuckerteam.chucker.api.Chucker
25
26
import com.chuckerteam.chucker.databinding.ChuckerActivityMainBinding
26
27
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
27
28
import com.chuckerteam.chucker.internal.data.model.DialogData
29
+ import com.chuckerteam.chucker.internal.support.FileSaver
28
30
import com.chuckerteam.chucker.internal.support.HarUtils
29
31
import com.chuckerteam.chucker.internal.support.Logger
30
32
import com.chuckerteam.chucker.internal.support.Sharable
31
33
import com.chuckerteam.chucker.internal.support.TransactionDetailsHarSharable
32
34
import com.chuckerteam.chucker.internal.support.TransactionListDetailsSharable
33
35
import com.chuckerteam.chucker.internal.support.shareAsFile
34
36
import com.chuckerteam.chucker.internal.support.showDialog
37
+ import com.chuckerteam.chucker.internal.ui.MainActivity.ExportType.HAR
38
+ import com.chuckerteam.chucker.internal.ui.MainActivity.ExportType.TEXT
35
39
import com.chuckerteam.chucker.internal.ui.transaction.TransactionActivity
36
40
import com.chuckerteam.chucker.internal.ui.transaction.TransactionAdapter
37
41
import com.google.android.material.snackbar.Snackbar
38
42
import kotlinx.coroutines.Dispatchers
39
43
import kotlinx.coroutines.launch
40
44
import kotlinx.coroutines.withContext
45
+ import okio.Source
46
+ import okio.buffer
47
+ import okio.source
41
48
42
49
internal class MainActivity :
43
50
BaseChuckerActivity (),
@@ -63,6 +70,16 @@ internal class MainActivity :
63
70
}
64
71
}
65
72
73
+ private val saveTextToFile =
74
+ registerForActivityResult(ActivityResultContracts .CreateDocument (TEXT .mimeType)) { uri ->
75
+ onSaveToFileActivityResult(uri, TEXT )
76
+ }
77
+
78
+ private val saveHarToFile =
79
+ registerForActivityResult(ActivityResultContracts .CreateDocument (HAR .mimeType)) { uri ->
80
+ onSaveToFileActivityResult(uri, HAR )
81
+ }
82
+
66
83
override fun onCreate (savedInstanceState : Bundle ? ) {
67
84
super .onCreate(savedInstanceState)
68
85
@@ -111,6 +128,7 @@ internal class MainActivity :
111
128
) == PackageManager .PERMISSION_GRANTED -> {
112
129
// We have permission, all good
113
130
}
131
+
114
132
shouldShowRequestPermissionRationale(Manifest .permission.POST_NOTIFICATIONS ) -> {
115
133
Snackbar .make(
116
134
mainBinding.root,
@@ -125,6 +143,7 @@ internal class MainActivity :
125
143
}
126
144
}.show()
127
145
}
146
+
128
147
else -> {
129
148
permissionRequest.launch(Manifest .permission.POST_NOTIFICATIONS )
130
149
}
@@ -133,6 +152,7 @@ internal class MainActivity :
133
152
134
153
override fun onCreateOptionsMenu (menu : Menu ): Boolean {
135
154
menuInflater.inflate(R .menu.chucker_transactions_list, menu)
155
+ MenuCompat .setGroupDividerEnabled(menu, true )
136
156
setUpSearch(menu)
137
157
return super .onCreateOptionsMenu(menu)
138
158
}
@@ -156,6 +176,7 @@ internal class MainActivity :
156
176
)
157
177
true
158
178
}
179
+
159
180
R .id.share_text -> {
160
181
showDialog(
161
182
getExportDialogData(R .string.chucker_export_text_http_confirmation),
@@ -168,6 +189,7 @@ internal class MainActivity :
168
189
)
169
190
true
170
191
}
192
+
171
193
R .id.share_har -> {
172
194
showDialog(
173
195
getExportDialogData(R .string.chucker_export_har_http_confirmation),
@@ -186,6 +208,17 @@ internal class MainActivity :
186
208
)
187
209
true
188
210
}
211
+
212
+ R .id.save_text -> {
213
+ showSaveDialog(TEXT )
214
+ true
215
+ }
216
+
217
+ R .id.save_har -> {
218
+ showSaveDialog(HAR )
219
+ true
220
+ }
221
+
189
222
else -> {
190
223
super .onOptionsItemSelected(item)
191
224
}
@@ -248,6 +281,93 @@ internal class MainActivity :
248
281
negativeButtonText = getString(R .string.chucker_cancel),
249
282
)
250
283
284
+ private fun getSaveDialogData (
285
+ @StringRes dialogMessage : Int ,
286
+ ): DialogData =
287
+ DialogData (
288
+ title = getString(R .string.chucker_save),
289
+ message = getString(dialogMessage),
290
+ positiveButtonText = getString(R .string.chucker_save),
291
+ negativeButtonText = getString(R .string.chucker_cancel),
292
+ )
293
+
294
+ private fun showSaveDialog (exportType : ExportType ) {
295
+ showDialog(
296
+ getSaveDialogData(
297
+ when (exportType) {
298
+ TEXT -> R .string.chucker_save_text_http_confirmation
299
+ HAR -> R .string.chucker_save_har_http_confirmation
300
+ },
301
+ ),
302
+ onPositiveClick = {
303
+ when (exportType) {
304
+ TEXT -> saveTextToFile.launch(EXPORT_TXT_FILE_NAME )
305
+ HAR -> saveHarToFile.launch(EXPORT_HAR_FILE_NAME )
306
+ }
307
+ },
308
+ onNegativeClick = null ,
309
+ )
310
+ }
311
+
312
+ private fun onSaveToFileActivityResult (
313
+ uri : Uri ? ,
314
+ exportType : ExportType ,
315
+ ) {
316
+ if (uri == null ) {
317
+ Toast .makeText(
318
+ applicationContext,
319
+ R .string.chucker_save_failed_to_open_document,
320
+ Toast .LENGTH_SHORT ,
321
+ ).show()
322
+ return
323
+ }
324
+ lifecycleScope.launch {
325
+ val source =
326
+ runCatching {
327
+ prepareDataToSave(exportType)
328
+ }.getOrNull() ? : return @launch
329
+ val result = FileSaver .saveFile(source, uri, contentResolver)
330
+ val toastMessageId =
331
+ if (result) {
332
+ R .string.chucker_file_saved
333
+ } else {
334
+ R .string.chucker_file_not_saved
335
+ }
336
+ Toast .makeText(applicationContext, toastMessageId, Toast .LENGTH_SHORT ).show()
337
+ }
338
+ }
339
+
340
+ private suspend fun prepareDataToSave (exportType : ExportType ): Source ? {
341
+ val transactions = viewModel.getAllTransactions()
342
+ if (transactions.isEmpty()) {
343
+ showToast(applicationContext.getString(R .string.chucker_save_empty_text))
344
+ return null
345
+ }
346
+ return withContext(Dispatchers .IO ) {
347
+ when (exportType) {
348
+ TEXT -> {
349
+ TransactionListDetailsSharable (
350
+ transactions,
351
+ encodeUrls = false ,
352
+ ).toSharableContent(this @MainActivity)
353
+ }
354
+
355
+ HAR -> {
356
+ HarUtils .harStringFromTransactions(
357
+ transactions,
358
+ getString(R .string.chucker_name),
359
+ getString(R .string.chucker_version),
360
+ ).byteInputStream().source().buffer()
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ private enum class ExportType (val mimeType : String ) {
367
+ TEXT (" text/plain" ),
368
+ HAR (" application/har+json" ),
369
+ }
370
+
251
371
companion object {
252
372
private const val EXPORT_TXT_FILE_NAME = " transactions.txt"
253
373
private const val EXPORT_HAR_FILE_NAME = " transactions.har"
0 commit comments