diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt index a731f532b..9c6d96897 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt @@ -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 @@ -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 @@ -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") diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransactionTuple.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransactionTuple.kt index a48be2c95..c6d6589c1 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransactionTuple.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/entity/HttpTransactionTuple.kt @@ -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. @@ -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 + } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt index 385061db1..ae5f41c1a 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt @@ -19,8 +19,9 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa return transcationDao.getFilteredTuples("$code%", pathQuery) } - override fun getTransaction(transactionId: Long): LiveData { - return transcationDao.getById(transactionId).distinctUntilChanged { old, new -> old.hasTheSameContent(new) } + override fun getTransaction(transactionId: Long): LiveData { + return transcationDao.getById(transactionId) + .distinctUntilChanged { old, new -> old?.hasTheSameContent(new) != false } } override fun getSortedTransactionTuples(): LiveData> { diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt index 89b40189c..86c73e539 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt @@ -23,5 +23,5 @@ internal interface HttpTransactionRepository { fun getFilteredTransactionTuples(code: String, path: String): LiveData> - fun getTransaction(transactionId: Long): LiveData + fun getTransaction(transactionId: Long): LiveData } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt index 8b7bcaee9..eabb5bcab 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt @@ -37,7 +37,7 @@ internal interface HttpTransactionDao { fun deleteAll() @Query("SELECT * FROM transactions WHERE id = :id") - fun getById(id: Long): LiveData + fun getById(id: Long): LiveData @Query("DELETE FROM transactions WHERE requestDate <= :threshold") fun deleteBefore(threshold: Long) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FormatUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FormatUtils.kt index e18d39ee0..c0c4184a4 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/FormatUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FormatUtils.kt @@ -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" diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FormattedUrl.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FormattedUrl.kt new file mode 100644 index 000000000..3e133f933 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FormattedUrl.kt @@ -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() + ) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/LiveDataUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/LiveDataUtils.kt index 5b79194f6..129ab5c61 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/LiveDataUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/LiveDataUtils.kt @@ -6,6 +6,40 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import java.util.concurrent.Executor +internal fun LiveData.combineLatest( + other: LiveData, + func: (T1, T2) -> R +): LiveData { + return MediatorLiveData().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 LiveData.combineLatest(other: LiveData): LiveData> { + 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. diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainViewModel.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainViewModel.kt index 9fc4cec89..51fe6f208 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainViewModel.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/MainViewModel.kt @@ -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 @@ -14,7 +14,7 @@ internal class MainViewModel : ViewModel() { private val currentFilter = MutableLiveData("") - val transactions: LiveData> = Transformations.switchMap(currentFilter) { searchQuery -> + val transactions: LiveData> = currentFilter.switchMap { searchQuery -> with(RepositoryProvider.transaction()) { when { searchQuery.isNullOrBlank() -> { @@ -30,12 +30,8 @@ internal class MainViewModel : ViewModel() { } } - val errors: LiveData> = - Transformations.map( - RepositoryProvider.throwable().getSortedThrowablesTuples() - ) { - it - } + val errors: LiveData> = RepositoryProvider.throwable() + .getSortedThrowablesTuples() fun updateItemsFilter(searchQuery: String) { currentFilter.value = searchQuery diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionActivity.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionActivity.kt index a8cb99c45..685f2cc4d 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionActivity.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionActivity.kt @@ -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 } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt index 79cf7d160..c716a74be 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt @@ -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 diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionListFragment.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionListFragment.kt index 5d455cd47..c26853a05 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionListFragment.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionListFragment.kt @@ -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 { @@ -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 { diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt index 1fc6039f9..e28649a6f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt @@ -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() { @@ -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 + } + ) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt index 93312550e..ce83b583d 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt @@ -71,6 +71,7 @@ internal class TransactionPayloadFragment : viewModel.transaction.observe( viewLifecycleOwner, Observer { transaction -> + if (transaction == null) return@Observer PayloadLoaderTask(this).execute(Pair(type, transaction)) } ) @@ -104,6 +105,8 @@ internal class TransactionPayloadFragment : } } + menu.findItem(R.id.encode_url).isVisible = false + super.onCreateOptionsMenu(menu, inflater) } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionViewModel.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionViewModel.kt index 732c6fd90..934502f8f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionViewModel.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionViewModel.kt @@ -1,28 +1,54 @@ package com.chuckerteam.chucker.internal.ui.transaction import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.map import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider +import com.chuckerteam.chucker.internal.support.combineLatest -internal class TransactionViewModel(transactionId: Long) : ViewModel() { +internal class TransactionViewModel( + transactionId: Long +) : ViewModel() { - val transactionTitle: LiveData = - Transformations.map(RepositoryProvider.transaction().getTransaction(transactionId)) { - if (it != null) "${it.method} ${it.path}" else "" + private val mutableEncodeUrl = MutableLiveData(false) + + val encodeUrl: LiveData = mutableEncodeUrl + + val transactionTitle: LiveData = RepositoryProvider.transaction() + .getTransaction(transactionId) + .combineLatest(encodeUrl) { transaction, encodeUrl -> + if (transaction != null) "${transaction.method} ${transaction.getFormattedPath(encode = encodeUrl)}" else "" } - val transaction: LiveData = - Transformations.map(RepositoryProvider.transaction().getTransaction(transactionId)) { - it + + val doesUrlRequireEncoding: LiveData = RepositoryProvider.transaction() + .getTransaction(transactionId) + .map { transaction -> + if (transaction == null) { + false + } else { + transaction.getFormattedPath(encode = true) != transaction.getFormattedPath(encode = false) + } } + + val transaction: LiveData = RepositoryProvider.transaction().getTransaction(transactionId) + + fun switchUrlEncoding() = encodeUrl(!encodeUrl.value!!) + + fun encodeUrl(encode: Boolean) { + mutableEncodeUrl.value = encode + } } -internal class TransactionViewModelFactory(private val transactionId: Long = 0L) : +internal class TransactionViewModelFactory( + private val transactionId: Long = 0L +) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { require(modelClass == TransactionViewModel::class.java) { "Cannot create $modelClass" } + @Suppress("UNCHECKED_CAST") return TransactionViewModel(transactionId) as T } } diff --git a/library/src/main/res/drawable/chucker_ic_decoded_url_white.xml b/library/src/main/res/drawable/chucker_ic_decoded_url_white.xml new file mode 100644 index 000000000..043a11f63 --- /dev/null +++ b/library/src/main/res/drawable/chucker_ic_decoded_url_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/library/src/main/res/drawable/chucker_ic_encoded_url_white.xml b/library/src/main/res/drawable/chucker_ic_encoded_url_white.xml new file mode 100644 index 000000000..1da285872 --- /dev/null +++ b/library/src/main/res/drawable/chucker_ic_encoded_url_white.xml @@ -0,0 +1,11 @@ + + + + diff --git a/library/src/main/res/drawable/chucker_ic_save_white.xml b/library/src/main/res/drawable/chucker_ic_save_white.xml index 56e36736f..b728b571b 100644 --- a/library/src/main/res/drawable/chucker_ic_save_white.xml +++ b/library/src/main/res/drawable/chucker_ic_save_white.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/library/src/main/res/menu/chucker_transaction.xml b/library/src/main/res/menu/chucker_transaction.xml index 4d724d784..a91874c0f 100644 --- a/library/src/main/res/menu/chucker_transaction.xml +++ b/library/src/main/res/menu/chucker_transaction.xml @@ -8,6 +8,13 @@ android:visible="false" app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="collapseActionView|ifRoom" /> + + - \ No newline at end of file + diff --git a/library/src/main/res/menu/chucker_transactions_list.xml b/library/src/main/res/menu/chucker_transactions_list.xml index fecb4a941..36f7203dd 100644 --- a/library/src/main/res/menu/chucker_transactions_list.xml +++ b/library/src/main/res/menu/chucker_transactions_list.xml @@ -10,4 +10,4 @@ android:id="@+id/clear" android:icon="@drawable/chucker_ic_delete_white" app:showAsAction="always" /> - \ No newline at end of file + diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index c81ab68a1..aec0c8129 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Chucker Recording HTTP activity Clear + Encode URL Cancel Overview Request diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/FormattedUrlTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/FormattedUrlTest.kt new file mode 100644 index 000000000..1d314415a --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/FormattedUrlTest.kt @@ -0,0 +1,97 @@ +package com.chuckerteam.chucker.internal.support + +import junit.framework.TestCase.assertEquals +import okhttp3.HttpUrl +import org.junit.Test + +class FormattedUrlTest { + @Test + fun encodedUrl_withAllParams_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com/path/to some/resource?q=\"Hello, world!\"") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = true) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("/path/to%20some/resource", formattedUrl.path) + assertEquals("q=%22Hello,%20world!%22", formattedUrl.query) + assertEquals("/path/to%20some/resource?q=%22Hello,%20world!%22", formattedUrl.pathWithQuery) + assertEquals( + "https://www.example.com/path/to%20some/resource?q=%22Hello,%20world!%22", + formattedUrl.url + ) + } + + @Test + fun encodedUrl_withoutPath_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com?q=\"Hello, world!\"") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = true) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("", formattedUrl.path) + assertEquals("q=%22Hello,%20world!%22", formattedUrl.query) + assertEquals("?q=%22Hello,%20world!%22", formattedUrl.pathWithQuery) + assertEquals("https://www.example.com?q=%22Hello,%20world!%22", formattedUrl.url) + } + + @Test + fun encodedUrl_withoutQuery_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com/path/to some/resource") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = true) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("/path/to%20some/resource", formattedUrl.path) + assertEquals("", formattedUrl.query) + assertEquals("/path/to%20some/resource", formattedUrl.pathWithQuery) + assertEquals("https://www.example.com/path/to%20some/resource", formattedUrl.url) + } + + @Test + fun decodedUrl_withAllParams_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com/path/to some/resource?q=\"Hello, world!\"") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = false) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("/path/to some/resource", formattedUrl.path) + assertEquals("q=\"Hello, world!\"", formattedUrl.query) + assertEquals("/path/to some/resource?q=\"Hello, world!\"", formattedUrl.pathWithQuery) + assertEquals( + "https://www.example.com/path/to some/resource?q=\"Hello, world!\"", + formattedUrl.url + ) + } + + @Test + fun decodedUrl_withoutPath_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com?q=\"Hello, world!\"") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = false) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("", formattedUrl.path) + assertEquals("q=\"Hello, world!\"", formattedUrl.query) + assertEquals("?q=\"Hello, world!\"", formattedUrl.pathWithQuery) + assertEquals("https://www.example.com?q=\"Hello, world!\"", formattedUrl.url) + } + + @Test + fun decodedUrl_withoutQuery_isFormattedProperly() { + val url = HttpUrl.get("https://www.example.com/path/to some/resource") + + val formattedUrl = FormattedUrl.fromHttpUrl(url, encoded = false) + + assertEquals("https", formattedUrl.scheme) + assertEquals("www.example.com", formattedUrl.host) + assertEquals("/path/to some/resource", formattedUrl.path) + assertEquals("", formattedUrl.query) + assertEquals("/path/to some/resource", formattedUrl.pathWithQuery) + assertEquals("https://www.example.com/path/to some/resource", formattedUrl.url) + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt new file mode 100644 index 000000000..40494c528 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt @@ -0,0 +1,72 @@ +package com.chuckerteam.chucker.internal.support + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData +import com.chuckerteam.chucker.test +import junit.framework.TestCase.assertEquals +import org.junit.Rule +import org.junit.Test + +class LiveDataCombineLatestTest { + @get:Rule val instantExecutorRule = InstantTaskExecutorRule() + + private val inputA = MutableLiveData() + private val inputB = MutableLiveData() + + private val upstream = inputA.combineLatest(inputB) + + @Test + fun firstEmptyValue_preventsDownstreamEmissions() { + upstream.test { + inputB.value = 1 + inputB.value = 2 + inputB.value = 3 + + expectNoData() + } + } + + @Test + fun secondEmptyValue_preventsDownstreamEmissions() { + upstream.test { + inputA.value = true + inputA.value = false + + expectNoData() + } + } + + @Test + fun bothEmittedValues_areCombinedDownstream() { + upstream.test { + inputA.value = true + inputB.value = 1 + + assertEquals(true to 1, expectData()) + } + } + + @Test + fun lastFirstValue_isCombinedWithNewestSecondValues() { + upstream.test { + inputA.value = true + inputB.value = 1 + assertEquals(true to 1, expectData()) + + inputB.value = 2 + assertEquals(true to 2, expectData()) + } + } + + @Test + fun lastSecondValue_isCombinedWithNewestFirstValues() { + upstream.test { + inputA.value = true + inputB.value = 1 + assertEquals(true to 1, expectData()) + + inputA.value = false + assertEquals(false to 1, expectData()) + } + } +} diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt index 9fe51b54b..ad790f8fb 100644 --- a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt @@ -86,6 +86,8 @@ class HttpBinClient( deny().enqueue(cb) cache("Mon").enqueue(cb) cache(30).enqueue(cb) + redirectTo("https://ascii.cl?parameter=%22Click+on+%27URL+Encode%27%21%22").enqueue(cb) + redirectTo("https://ascii.cl?parameter=\"Click on 'URL Encode'!\"").enqueue(cb) } }