Skip to content

Commit

Permalink
Ability to export the list of transactions (#305)
Browse files Browse the repository at this point in the history
* added export feature for chucker

formatting

* addressing review comments

* addressing indentation issues

addressing indentation issues

* updating failing test

* renaming methods in filefactory to make it more generic

* refactored exportTransactions() to make it easy to read

* updated coroutines with viewmodelscope.

* removed runblocking and added lifecycle-runtime extension.

* updated share compat to remove error log

* removed thread switching and added suspend modifier for getStringFromTransactions method

Co-authored-by: Karthik R <karthr@paypal.com>
  • Loading branch information
adb-shell and Karthik R authored Apr 23, 2020
1 parent 5ff8c92 commit 392a63b
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 26 deletions.
10 changes: 10 additions & 0 deletions library/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,15 @@
<service
android:name="com.chuckerteam.chucker.internal.support.ClearDatabaseService"
android:exported="false" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/chucker_provider_authority"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chuckerteam.chucker.internal.data.model

internal data class DialogData(
val title: String,
val message: String,
val postiveButtonText: String?,
val negativeButtonText: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa
override suspend fun deleteOldTransactions(threshold: Long) {
transactionDao.deleteBefore(threshold)
}

override suspend fun getAllTransactions(): List<HttpTransaction> = transactionDao.getAll()
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ internal interface HttpTransactionRepository {
fun getFilteredTransactionTuples(code: String, path: String): LiveData<List<HttpTransactionTuple>>

fun getTransaction(transactionId: Long): LiveData<HttpTransaction?>

suspend fun getAllTransactions(): List<HttpTransaction>
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ internal interface HttpTransactionDao {

@Query("DELETE FROM transactions WHERE requestDate <= :threshold")
suspend fun deleteBefore(threshold: Long)

@Query("SELECT * FROM transactions")
suspend fun getAll(): List<HttpTransaction>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import android.content.Context
import java.io.File
import java.util.concurrent.atomic.AtomicLong

internal const val EXPORT_FILENAME = "transactions.txt"

internal class AndroidCacheFileFactory(
context: Context
) : FileFactory {
private val fileDir = context.cacheDir
private val uniqueIdGenerator = AtomicLong()

override fun create(): File {
return File(fileDir, "chucker-${uniqueIdGenerator.getAndIncrement()}")
override fun create() = create(filename = "chucker-${uniqueIdGenerator.getAndIncrement()}")

override fun create(filename: String): File = File(fileDir, filename).apply {
if (exists()) {
delete()
}
createNewFile()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.chuckerteam.chucker.internal.support

import android.content.Context
import com.chuckerteam.chucker.internal.data.model.DialogData
import com.google.android.material.dialog.MaterialAlertDialogBuilder

internal fun Context.showDialog(
dialogData: DialogData,
onPositiveClick: (() -> Unit)?,
onNegativeClick: (() -> Unit)?
) {
MaterialAlertDialogBuilder(this)
.setTitle(dialogData.title)
.setMessage(dialogData.message)
.setPositiveButton(dialogData.postiveButtonText) { _, _ ->
onPositiveClick?.invoke()
}
.setNegativeButton(dialogData.negativeButtonText) { _, _ ->
onNegativeClick?.invoke()
}
.show()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import java.io.File

internal interface FileFactory {
fun create(): File
fun create(filename: String): File
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.chuckerteam.chucker.internal.support

import android.content.Context
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

internal object ShareUtils {
suspend fun getStringFromTransactions(transactions: List<HttpTransaction>, context: Context): String {
return withContext(Dispatchers.Default) {
transactions.joinToString(
separator = "\n${context.getString(R.string.chucker_export_separator)}\n",
prefix = "${context.getString(R.string.chucker_export_prefix)}\n",
postfix = "\n${context.getString(R.string.chucker_export_postfix)}\n"
) { FormatUtils.getShareText(context, it, false) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple
import com.chuckerteam.chucker.internal.data.entity.RecordedThrowableTuple
import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider
import com.chuckerteam.chucker.internal.support.EXPORT_FILENAME
import com.chuckerteam.chucker.internal.support.FileFactory
import com.chuckerteam.chucker.internal.support.NotificationHelper
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

internal class MainViewModel : ViewModel() {

Expand All @@ -35,6 +41,14 @@ internal class MainViewModel : ViewModel() {
val throwables: LiveData<List<RecordedThrowableTuple>> = RepositoryProvider.throwable()
.getSortedThrowablesTuples()

suspend fun getAllTransactions(): List<HttpTransaction>? = RepositoryProvider.transaction().getAllTransactions()

suspend fun createExportFile(content: String, fileFactory: FileFactory): File = withContext(Dispatchers.IO) {
val file = fileFactory.create(EXPORT_FILENAME)
file.writeText(content)
return@withContext file
}

fun updateItemsFilter(searchQuery: String) {
currentFilter.value = searchQuery
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.databinding.ChuckerFragmentThrowableListBinding
import com.chuckerteam.chucker.internal.data.model.DialogData
import com.chuckerteam.chucker.internal.support.showDialog
import com.chuckerteam.chucker.internal.ui.MainViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder

internal class ThrowableListFragment : Fragment(), ThrowableAdapter.ThrowableClickListListener {

Expand Down Expand Up @@ -75,14 +76,19 @@ internal class ThrowableListFragment : Fragment(), ThrowableAdapter.ThrowableCli
}

private fun askForConfirmation() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chucker_clear)
.setMessage(R.string.chucker_clear_throwable_confirmation)
.setPositiveButton(R.string.chucker_clear) { _, _ ->
val confirmationDialogData = DialogData(
title = getString(R.string.chucker_clear),
message = getString(R.string.chucker_clear_throwable_confirmation),
postiveButtonText = getString(R.string.chucker_clear),
negativeButtonText = getString(R.string.chucker_cancel)
)
requireContext().showDialog(
confirmationDialogData,
onPositiveClick = {
viewModel.clearThrowables()
}
.setNegativeButton(R.string.chucker_cancel, null)
.show()
},
onNegativeClick = null
)
}

override fun onThrowableClick(throwableId: Long, position: Int) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.chuckerteam.chucker.internal.ui.transaction

import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
Expand All @@ -8,15 +11,25 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.databinding.ChuckerFragmentTransactionListBinding
import com.chuckerteam.chucker.internal.data.model.DialogData
import com.chuckerteam.chucker.internal.support.AndroidCacheFileFactory
import com.chuckerteam.chucker.internal.support.FileFactory
import com.chuckerteam.chucker.internal.support.ShareUtils
import com.chuckerteam.chucker.internal.support.showDialog
import com.chuckerteam.chucker.internal.ui.MainViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

internal class TransactionListFragment :
Fragment(),
Expand All @@ -26,6 +39,10 @@ internal class TransactionListFragment :
private lateinit var viewModel: MainViewModel
private lateinit var transactionsBinding: ChuckerFragmentTransactionListBinding
private lateinit var transactionsAdapter: TransactionAdapter
private val cacheFileFactory: FileFactory by lazy {
AndroidCacheFileFactory(requireContext())
}
private val uiScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -65,6 +82,11 @@ internal class TransactionListFragment :
)
}

override fun onDestroy() {
super.onDestroy()
uiScope.cancel()
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.chucker_transactions_list, menu)
setUpSearch(menu)
Expand All @@ -79,20 +101,30 @@ internal class TransactionListFragment :
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.clear) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chucker_clear)
.setMessage(R.string.chucker_clear_http_confirmation)
.setPositiveButton(
R.string.chucker_clear
) { _, _ ->
viewModel.clearTransactions()
}
.setNegativeButton(R.string.chucker_cancel, null)
.show()
true
} else {
super.onOptionsItemSelected(item)
return when (item.itemId) {
R.id.clear -> {
requireContext().showDialog(
getClearDialogData(),
onPositiveClick = {
viewModel.clearTransactions()
},
onNegativeClick = null
)
true
}
R.id.export -> {
requireContext().showDialog(
getExportDialogData(),
onPositiveClick = {
exportTransactions()
},
onNegativeClick = null
)
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}

Expand All @@ -107,6 +139,54 @@ internal class TransactionListFragment :
TransactionActivity.start(requireActivity(), transactionId)
}

private fun exportTransactions() {
uiScope.launch {
val transactions = viewModel.getAllTransactions()
if (transactions.isNullOrEmpty()) {
Toast.makeText(requireContext(), R.string.chucker_export_empty_text, Toast.LENGTH_SHORT).show()
} else {
val filecontent = ShareUtils.getStringFromTransactions(transactions, requireContext())
val file = viewModel.createExportFile(filecontent, cacheFileFactory)
val uri = FileProvider.getUriForFile(
requireContext(),
getString(R.string.chucker_provider_authority),
file
)
shareFile(uri)
}
}
}

private fun shareFile(uri: Uri) {
val sendIntent = ShareCompat.IntentBuilder.from(requireActivity())
.setType(requireContext().contentResolver.getType(uri))
.setChooserTitle(getString(R.string.chucker_share_all_transactions_title))
.setSubject(getString(R.string.chucker_share_all_transactions_subject))
.setStream(uri)
.intent

sendIntent.apply {
clipData = ClipData.newRawUri("transactions", uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

startActivity(Intent.createChooser(sendIntent, getString(R.string.chucker_share_all_transactions_title)))
}

private fun getClearDialogData(): DialogData = DialogData(
title = getString(R.string.chucker_clear),
message = getString(R.string.chucker_clear_http_confirmation),
postiveButtonText = getString(R.string.chucker_clear),
negativeButtonText = getString(R.string.chucker_cancel)
)

private fun getExportDialogData(): DialogData = DialogData(
title = getString(R.string.chucker_export),
message = getString(R.string.chucker_export_http_confirmation),
postiveButtonText = getString(R.string.chucker_export),
negativeButtonText = getString(R.string.chucker_cancel)
)

companion object {
fun newInstance(): TransactionListFragment {
return TransactionListFragment()
Expand Down
4 changes: 4 additions & 0 deletions library/src/main/res/menu/chucker_transactions_list.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
android:icon="@drawable/chucker_ic_search_white"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
<item android:id="@+id/export"
android:icon="@drawable/chucker_ic_share_white"
android:title="@string/chucker_export"
app:showAsAction="ifRoom" />
<item android:title="@string/chucker_clear"
android:id="@+id/clear"
android:icon="@drawable/chucker_ic_delete_white"
Expand Down
9 changes: 9 additions & 0 deletions library/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<string name="chucker_yes">Yes</string>
<string name="chucker_no">No</string>
<string name="chucker_share">Share</string>
<string name="chucker_export">Export</string>
<string name="chucker_export_empty_text">Nothing to export</string>
<string name="chucker_share_as_text">Share as text</string>
<string name="chucker_share_as_curl">Share as curl command</string>
<string name="chucker_save">Save body to file</string>
Expand All @@ -43,15 +45,22 @@
<string name="chucker_throwable_notification_category">Chucker throwables</string>
<string name="chucker_share_throwable_title">Share throwable details</string>
<string name="chucker_share_transaction_title">Share transaction details</string>
<string name="chucker_share_all_transactions_title">Share transactions</string>
<string name="chucker_share_throwable_subject">Throwable details</string>
<string name="chucker_share_transaction_subject">Transaction details</string>
<string name="chucker_share_all_transactions_subject">All transactions</string>
<string name="chucker_share_throwable_content"><![CDATA[Date: %1$s\nException: %2$s\nTag: %3$s\nMessage: %4$s\n\n%5$s]]></string>
<string name="chucker_clear_http_confirmation">Do you want to clear complete network calls history?</string>
<string name="chucker_clear_throwable_confirmation">Do you want to clear complete throwables history?</string>
<string name="chucker_export_http_confirmation">Do you want to export all network transactions?</string>
<string name="chucker_export_separator">==================</string>
<string name="chucker_export_prefix">/* Export Start */</string>
<string name="chucker_export_postfix">/* Export End */</string>
<string name="chucker_check_readme"><a href="https://github.com/ChuckerTeam/chucker/blob/develop/README.md#configure-">Check the setup instructions on GitHub</a></string>
<string name="chucker_setup">Setup</string>
<string name="chucker_binary_data">binary data</string>
<string name="chucker_request_not_ready">The request isn\'t ready for sharing or saving</string>
<string name="chucker_request_is_empty">This request is empty</string>
<string name="chucker_response_is_empty">This response is empty</string>
<string name="chucker_provider_authority">com.chuckerteam.chucker.provider</string>
</resources>
4 changes: 4 additions & 0 deletions library/src/main/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="export" path="."/>
</paths>
Loading

0 comments on commit 392a63b

Please sign in to comment.