Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switching between encoded and decoded URL formats. #233

Merged
merged 10 commits into from
Feb 23, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.chuckerteam.chucker.internal.support.FormatUtils
import com.chuckerteam.chucker.internal.support.FormattedUrl
import com.chuckerteam.chucker.internal.support.JsonConverter
import com.google.gson.reflect.TypeToken
import java.util.Date
Expand Down Expand Up @@ -46,7 +47,6 @@ internal class HttpTransaction(
@ColumnInfo(name = "responseBody") var responseBody: String?,
@ColumnInfo(name = "isResponseBodyPlainText") var isResponseBodyPlainText: Boolean = true,
@ColumnInfo(name = "responseImageData") var responseImageData: ByteArray?

) {

@Ignore
Expand Down Expand Up @@ -208,13 +208,27 @@ internal class HttpTransaction(
}

fun populateUrl(url: HttpUrl): HttpTransaction {
this.url = url.toString()
host = url.host()
path = ("/${url.pathSegments().joinToString("/")}${url.query()?.let { "?$it" } ?: ""}")
scheme = url.scheme()
val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = false)
this.url = formattedUrl.url
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could rename url parameter for this function, so there will be no need in using this.url for this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about httpUrl?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me 👍

host = formattedUrl.host
path = formattedUrl.pathWithQuery
scheme = formattedUrl.scheme
return this
}

fun getFormattedUrl(encode: Boolean): String {
val url = this.url ?: return ""
val httpUrl = HttpUrl.get(url)
return FormattedUrl.fromHttpUrl(httpUrl, encode).url
}

fun getFormattedPath(encode: Boolean): String {
val url = this.url ?: return ""

val httpUrl = HttpUrl.parse(url) ?: return ""
return FormattedUrl.fromHttpUrl(httpUrl, encode).pathWithQuery
}

// Not relying on 'equals' because comparison be long due to request and response sizes
// and it would be unwise to do this every time 'equals' is called.
@Suppress("ComplexMethod")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.chuckerteam.chucker.internal.data.entity

import androidx.room.ColumnInfo
import com.chuckerteam.chucker.internal.support.FormatUtils
import com.chuckerteam.chucker.internal.support.FormattedUrl
import okhttp3.HttpUrl

/**
* A subset of [HttpTransaction] to perform faster Read operations on the Repository.
Expand Down Expand Up @@ -42,4 +44,15 @@ internal class HttpTransactionTuple(
private fun formatBytes(bytes: Long): String {
return FormatUtils.formatByteCount(bytes, true)
}

fun getFormattedPath(encode: Boolean): String {
val path = this.path ?: return ""

// Create dummy URL since there is no data in this class to get it from
// and we are only interested in a formatted path with query.
val dummyUrl = "https://www.example.com$path"

val httpUrl = HttpUrl.parse(dummyUrl) ?: return ""
return FormattedUrl.fromHttpUrl(httpUrl, encode).pathWithQuery
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa
return transcationDao.getFilteredTuples("$code%", pathQuery)
}

override fun getTransaction(transactionId: Long): LiveData<HttpTransaction> {
return transcationDao.getById(transactionId).distinctUntilChanged { old, new -> old.hasTheSameContent(new) }
override fun getTransaction(transactionId: Long): LiveData<HttpTransaction?> {
return transcationDao.getById(transactionId)
.distinctUntilChanged { old, new -> old?.hasTheSameContent(new) != false }
}

override fun getSortedTransactionTuples(): LiveData<List<HttpTransactionTuple>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ internal interface HttpTransactionRepository {

fun getFilteredTransactionTuples(code: String, path: String): LiveData<List<HttpTransactionTuple>>

fun getTransaction(transactionId: Long): LiveData<HttpTransaction>
fun getTransaction(transactionId: Long): LiveData<HttpTransaction?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal interface HttpTransactionDao {
fun deleteAll()

@Query("SELECT * FROM transactions WHERE id = :id")
fun getById(id: Long): LiveData<HttpTransaction>
fun getById(id: Long): LiveData<HttpTransaction?>

@Query("DELETE FROM transactions WHERE requestDate <= :threshold")
fun deleteBefore(threshold: Long)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ internal object FormatUtils {
}
}

fun getShareText(context: Context, transaction: HttpTransaction): String {
var text = "${context.getString(R.string.chucker_url)}: ${transaction.url}\n"
fun getShareText(context: Context, transaction: HttpTransaction, encodeUrls: Boolean): String {
var text = "${context.getString(R.string.chucker_url)}: ${transaction.getFormattedUrl(encodeUrls)}\n"
text += "${context.getString(R.string.chucker_method)}: ${transaction.method}\n"
text += "${context.getString(R.string.chucker_protocol)}: ${transaction.protocol}\n"
text += "${context.getString(R.string.chucker_status)}: ${transaction.status}\n"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.chuckerteam.chucker.internal.support

import okhttp3.HttpUrl

internal class FormattedUrl private constructor(
val scheme: String,
val host: String,
val path: String,
val query: String
) {
val pathWithQuery: String
get() = if (query.isBlank()) {
path
} else {
"$path?$query"
}

val url get() = "$scheme://$host$pathWithQuery"

companion object {
fun fromHttpUrl(httpUrl: HttpUrl, encoded: Boolean): FormattedUrl {
return if (encoded) {
encodedUrl(httpUrl)
} else {
decodedUrl(httpUrl)
}
}

private fun encodedUrl(httpUrl: HttpUrl): FormattedUrl {
val path = httpUrl.encodedPathSegments().joinToString("/")
return FormattedUrl(
httpUrl.scheme(),
httpUrl.host(),
if (path.isNotBlank()) "/$path" else "",
httpUrl.encodedQuery().orEmpty()
)
}

private fun decodedUrl(httpUrl: HttpUrl): FormattedUrl {
val path = httpUrl.pathSegments().joinToString("/")
return FormattedUrl(
httpUrl.scheme(),
httpUrl.host(),
if (path.isNotBlank()) "/$path" else "",
httpUrl.query().orEmpty()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import java.util.concurrent.Executor

internal fun <T1, T2, R> LiveData<T1>.combineLatest(
other: LiveData<T2>,
func: (T1, T2) -> R
): LiveData<R> {
return MediatorLiveData<R>().apply {
var lastA: T1? = null
var lastB: T2? = null

addSource(this@combineLatest) {
lastA = it
val observedB = lastB
if (it == null && value != null) {
value = null
} else if (it != null && observedB != null) {
value = func(it, observedB)
}
}

addSource(other) {
lastB = it
val observedA = lastA
if (it == null && value != null) {
value = null
} else if (observedA != null && it != null) {
value = func(observedA, it)
}
}
}
}

internal fun <T1, T2> LiveData<T1>.combineLatest(other: LiveData<T2>): LiveData<Pair<T1, T2>> {
return combineLatest(other) { a, b -> a to b }
}

// Unlike built-in extension operation is performed on a provided thread pool.
// This is needed in our case since we compare requests and responses which can be big
// and result in frame drops.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.chuckerteam.chucker.internal.ui
import android.text.TextUtils
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple
import com.chuckerteam.chucker.internal.data.entity.RecordedThrowableTuple
import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider
Expand All @@ -14,7 +14,7 @@ internal class MainViewModel : ViewModel() {

private val currentFilter = MutableLiveData<String>("")

val transactions: LiveData<List<HttpTransactionTuple>> = Transformations.switchMap(currentFilter) { searchQuery ->
val transactions: LiveData<List<HttpTransactionTuple>> = currentFilter.switchMap { searchQuery ->
with(RepositoryProvider.transaction()) {
when {
searchQuery.isNullOrBlank() -> {
Expand All @@ -30,12 +30,8 @@ internal class MainViewModel : ViewModel() {
}
}

val errors: LiveData<List<RecordedThrowableTuple>> =
Transformations.map(
RepositoryProvider.throwable().getSortedThrowablesTuples()
) {
it
}
val errors: LiveData<List<RecordedThrowableTuple>> = RepositoryProvider.throwable()
.getSortedThrowablesTuples()

fun updateItemsFilter(searchQuery: String) {
currentFilter.value = searchQuery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,34 @@ internal class TransactionActivity : BaseChuckerActivity() {

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.chucker_transaction, menu)
setUpUrlEncoding(menu)
return super.onCreateOptionsMenu(menu)
}

private fun setUpUrlEncoding(menu: Menu) {
val encodeUrlMenuItem = menu.findItem(R.id.encode_url)
encodeUrlMenuItem.setOnMenuItemClickListener {
viewModel.switchUrlEncoding()
return@setOnMenuItemClickListener true
}
viewModel.encodeUrl.observe(
this,
Observer { encode ->
val icon = if (encode) {
R.drawable.chucker_ic_encoded_url_white
} else {
R.drawable.chucker_ic_decoded_url_white
}
encodeUrlMenuItem.setIcon(icon)
}
)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.share_text -> {
viewModel.transaction.value?.let {
share(getShareText(this, it))
share(getShareText(this, it, viewModel.encodeUrl.value!!))
} ?: showToast(getString(R.string.chucker_request_not_ready))
true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal class TransactionAdapter internal constructor(

@SuppressLint("SetTextI18n")
fun bind(transaction: HttpTransactionTuple) {
path.text = "${transaction.method} ${transaction.path}"
path.text = "${transaction.method} ${transaction.getFormattedPath(encode = false)}"
host.text = transaction.host
start.text = DateFormat.getTimeInstance().format(transaction.requestDate)
ssl.visibility = if (transaction.isSsl) View.VISIBLE else View.GONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,15 @@ internal class TransactionListFragment :

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.chucker_transactions_list, menu)
setUpSearch(menu)
super.onCreateOptionsMenu(menu, inflater)
}

private fun setUpSearch(menu: Menu) {
val searchMenuItem = menu.findItem(R.id.search)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(true)
super.onCreateOptionsMenu(menu, inflater)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
Expand Down Expand Up @@ -99,8 +103,9 @@ internal class TransactionListFragment :
return true
}

override fun onTransactionClick(transactionId: Long, position: Int) =
override fun onTransactionClick(transactionId: Long, position: Int) {
TransactionActivity.start(requireActivity(), transactionId)
}

companion object {
fun newInstance(): TransactionListFragment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.internal.support.combineLatest

internal class TransactionOverviewFragment : Fragment() {

Expand Down Expand Up @@ -56,30 +57,36 @@ internal class TransactionOverviewFragment : Fragment() {
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val saveMenuItem = menu.findItem(R.id.save_body)
saveMenuItem.isVisible = false
menu.findItem(R.id.save_body).isVisible = false
viewModel.doesUrlRequireEncoding.observe(
viewLifecycleOwner,
Observer { menu.findItem(R.id.encode_url).isVisible = it }
)

super.onCreateOptionsMenu(menu, inflater)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.transaction.observe(
viewLifecycleOwner,
Observer { transaction ->
url.text = transaction.url
method.text = transaction.method
protocol.text = transaction.protocol
status.text = transaction.status.toString()
response.text = transaction.responseSummaryText
ssl.setText(if (transaction.isSsl) R.string.chucker_yes else R.string.chucker_no)
requestTime.text = transaction.requestDateString
responseTime.text = transaction.responseDateString
duration.text = transaction.durationString
requestSize.text = transaction.requestSizeString
responseSize.text = transaction.responseSizeString
totalSize.text = transaction.totalSizeString
}
)

viewModel.transaction
.combineLatest(viewModel.encodeUrl)
.observe(
viewLifecycleOwner,
Observer { (transaction, encodeUrl) ->
url.text = transaction?.getFormattedUrl(encodeUrl)
method.text = transaction?.method
protocol.text = transaction?.protocol
status.text = transaction?.status?.toString()
response.text = transaction?.responseSummaryText
ssl.setText(if (transaction?.isSsl == true) R.string.chucker_yes else R.string.chucker_no)
requestTime.text = transaction?.requestDateString
responseTime.text = transaction?.responseDateString
duration.text = transaction?.durationString
requestSize.text = transaction?.requestSizeString
responseSize.text = transaction?.responseSizeString
totalSize.text = transaction?.totalSizeString
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ internal class TransactionPayloadFragment :
viewModel.transaction.observe(
viewLifecycleOwner,
Observer { transaction ->
if (transaction == null) return@Observer
PayloadLoaderTask(this).execute(Pair(type, transaction))
}
)
Expand Down Expand Up @@ -104,6 +105,8 @@ internal class TransactionPayloadFragment :
}
}

menu.findItem(R.id.encode_url).isVisible = false

super.onCreateOptionsMenu(menu, inflater)
}

Expand Down
Loading