Skip to content

Commit

Permalink
Add switching between encoded and decoded URL formats. (#233)
Browse files Browse the repository at this point in the history
* Add switching between encoded and decoded URL formats.
* Make URL encoding transaction specific.
* Change test name for upcoming #244 PR.
* Use LiveDataRecord for combineLatest tests.
* Properly switch encoding and decoding URL.
* Show encoding icon only when it is viable.
* Add encoded URL samples to HttpBinClient.
* Avoid using 'this' scoping mechanism for URL.
  • Loading branch information
MiSikora authored Feb 23, 2020
1 parent 667d668 commit bee8813
Show file tree
Hide file tree
Showing 24 changed files with 420 additions and 55 deletions.
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 @@ -207,14 +207,25 @@ internal class HttpTransaction(
return responseBody?.let { formatBody(it, responseContentType) } ?: ""
}

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

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

fun getFormattedPath(encode: Boolean): String {
val httpUrl = url?.let(HttpUrl::get) ?: 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

0 comments on commit bee8813

Please sign in to comment.