From 751910f0fa6f001f0a1c5eabc210daa5317acab0 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Sat, 1 Aug 2020 13:44:29 -0500 Subject: [PATCH 01/10] WIP POC Chucker har file export. --- .../chucker/internal/data/har/Cookie.kt | 8 ++++ .../chucker/internal/data/har/Creator.kt | 8 ++++ .../chucker/internal/data/har/Entry.kt | 26 +++++++++++++ .../chucker/internal/data/har/Har.kt | 31 ++++++++++++++++ .../chucker/internal/data/har/Header.kt | 8 ++++ .../chucker/internal/data/har/Log.kt | 9 +++++ .../chucker/internal/data/har/PostData.kt | 27 ++++++++++++++ .../chucker/internal/data/har/QueryString.kt | 23 ++++++++++++ .../chucker/internal/data/har/Request.kt | 36 ++++++++++++++++++ .../chucker/internal/data/har/Response.kt | 37 +++++++++++++++++++ .../chucker/internal/data/har/Timings.kt | 9 +++++ .../support/AndroidCacheFileFactory.kt | 2 +- .../chucker/internal/support/JsonConverter.kt | 7 ++++ .../chucker/internal/support/ShareUtils.kt | 7 ++++ .../ui/transaction/TransactionActivity.kt | 7 ++++ .../ui/transaction/TransactionListFragment.kt | 2 +- .../src/main/res/menu/chucker_transaction.xml | 3 ++ library/src/main/res/values/strings.xml | 1 + 18 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Cookie.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Creator.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Header.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Log.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/data/har/Timings.kt diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Cookie.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Cookie.kt new file mode 100644 index 000000000..1193ce911 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Cookie.kt @@ -0,0 +1,8 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName + +internal data class Cookie( + @SerializedName("name") val name: String, + @SerializedName("value") val value: String +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Creator.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Creator.kt new file mode 100644 index 000000000..f67ba0e49 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Creator.kt @@ -0,0 +1,8 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName + +internal data class Creator( + @SerializedName("name") val name: String, + @SerializedName("version") val version: String +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt new file mode 100644 index 000000000..d60269aaf --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt @@ -0,0 +1,26 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.google.gson.annotations.SerializedName +import java.util.Date + +internal data class Entry( + @SerializedName("startedDateTime") val startedDateTime: String, + @SerializedName("time") val time: Long, + @SerializedName("request") val request: Request?, + @SerializedName("response") val response: Response? +) { + companion object { + fun fromHttpTransaction(transaction: HttpTransaction): Entry = Entry( + startedDateTime = transaction.requestDate.harFormatted(), + time = transaction.tookMs ?: 0, + request = Request.fromHttpTransaction(transaction), + response = Response.fromHttpTransaction(transaction) + ) + + private fun Long?.harFormatted(): String { + val date = if (this == null) Date() else Date(this) + return Har.DateFormat.get()!!.format(date) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt new file mode 100644 index 000000000..0ea1db097 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt @@ -0,0 +1,31 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.BuildConfig +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.google.gson.annotations.SerializedName +import java.text.SimpleDateFormat +import java.util.Locale + +// http://www.softwareishard.com/blog/har-12-spec/ +internal data class Har( + @SerializedName("log") val log: Log +) { + object DateFormat : ThreadLocal() { + override fun initialValue(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + } + + companion object { + fun fromHttpTransactions(transactions: List): Har { + return Har( + log = Log( + version = "1.2", + creator = Creator( + name = BuildConfig.LIBRARY_PACKAGE_NAME, + version = BuildConfig.VERSION_NAME + ), + entries = transactions.map(Entry.Companion::fromHttpTransaction).filter { it.response != null } + ) + ) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Header.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Header.kt new file mode 100644 index 000000000..d35a8a2bb --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Header.kt @@ -0,0 +1,8 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName + +internal data class Header( + @SerializedName("name") val name: String, + @SerializedName("value") val value: String +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Log.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Log.kt new file mode 100644 index 000000000..30792723b --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Log.kt @@ -0,0 +1,9 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName + +internal data class Log( + @SerializedName("version") val version: String, + @SerializedName("creator") val creator: Creator, + @SerializedName("entries") val entries: List +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt new file mode 100644 index 000000000..077602a75 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt @@ -0,0 +1,27 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.google.gson.annotations.SerializedName + +internal data class PostData( + @SerializedName("mimeType") val mimeType: String, + @SerializedName("text") val text: String +) { + companion object { + fun responsePostData(transaction: HttpTransaction): PostData? { + if (transaction.responseContentType == null || transaction.responseBody == null) return null + return PostData( + mimeType = transaction.responseContentType!!, + text = transaction.responseBody!! + ) + } + + fun requestPostData(transaction: HttpTransaction): PostData? { + if (transaction.requestContentType == null || transaction.requestBody == null) return null + return PostData( + mimeType = transaction.requestContentType!!, + text = transaction.requestBody!! + ) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt new file mode 100644 index 000000000..43cc4691b --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt @@ -0,0 +1,23 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName +import okhttp3.HttpUrl + +internal data class QueryString( + @SerializedName("name") val name: String, + @SerializedName("value") val value: String +) { + companion object { + fun fromUrl(url: HttpUrl): List { + val querySize = url.querySize() + val list = ArrayList() + (0 until querySize).forEach { index -> + list += QueryString( + name = url.queryParameterName(index), + value = url.queryParameterValue(index) + ) + } + return list + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt new file mode 100644 index 000000000..16b846ec8 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt @@ -0,0 +1,36 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.google.gson.annotations.SerializedName +import okhttp3.HttpUrl + +internal data class Request( + @SerializedName("method") val method: String, + @SerializedName("url") val url: String, + @SerializedName("httpVersion") val httpVersion: String, + @SerializedName("cookies") val cookies: List, + @SerializedName("headers") val headers: List
, + @SerializedName("queryString") val queryString: List, + @SerializedName("postData") val postData: PostData?, + @SerializedName("headerSize") val headerSize: Int, + @SerializedName("bodySize") val bodySize: Long +) { + companion object { + fun fromHttpTransaction(transaction: HttpTransaction): Request? { + if (transaction.requestDate == null) { + return null + } + return Request( + method = transaction.method!!, + url = transaction.url!!, + httpVersion = transaction.protocol ?: "HTTP/1.1", // TODO: This is wrong! This is the response protocol. + cookies = emptyList(), // TODO: Grab from headers? + headers = transaction.getParsedRequestHeaders()!!.map { Header(it.name, it.value) }, + queryString = QueryString.fromUrl(HttpUrl.get(transaction.url!!)), + postData = PostData.requestPostData(transaction), + headerSize = transaction.requestHeaders!!.length, + bodySize = transaction.requestPayloadSize!! + ) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt new file mode 100644 index 000000000..7c8ce4817 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt @@ -0,0 +1,37 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.google.gson.annotations.SerializedName + +internal data class Response( + @SerializedName("status") val status: Int, + @SerializedName("statusText") val statusText: String, + @SerializedName("httpVersion") val httpVersion: String, + @SerializedName("cookies") val cookies: List, + @SerializedName("headers") val headers: List
, + @SerializedName("content") val content: PostData?, + @SerializedName("redirectURL") val redirectUrl: String, + @SerializedName("headerSize") val headerSize: Int, + @SerializedName("bodySize") val bodySize: Long, + @SerializedName("timings") val timings: Timings +) { + companion object { + fun fromHttpTransaction(transaction: HttpTransaction): Response? { + if (transaction.responseDate == null) { + return null + } + return Response( + status = transaction.responseCode!!, + statusText = transaction.responseMessage!!, + httpVersion = transaction.protocol ?: "HTTP/1.1", // TODO: This is actually unknown + cookies = emptyList(), // TODO: Grab this from headers? + headers = transaction.getParsedResponseHeaders()!!.map { Header(it.name, it.value) }, + content = PostData.responsePostData(transaction), + redirectUrl = "", // TODO: We could maybe get this off the response headers Location header? + headerSize = transaction.responseHeaders!!.length, + bodySize = transaction.responsePayloadSize ?: 0, + timings = Timings(0, 0, transaction.tookMs ?: 0) // TODO: We need more detailed values here! + ) + } + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Timings.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Timings.kt new file mode 100644 index 000000000..b4d812ef3 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Timings.kt @@ -0,0 +1,9 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.gson.annotations.SerializedName + +internal data class Timings( + @SerializedName("send") val send: Long, + @SerializedName("wait") val wait: Long, + @SerializedName("receive") val receive: Long +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt index 1ab15dc8f..028749e3f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt @@ -4,7 +4,7 @@ import android.content.Context import java.io.File import java.util.concurrent.atomic.AtomicLong -internal const val EXPORT_FILENAME = "transactions.txt" +internal const val EXPORT_FILENAME = "transactions.har" internal class AndroidCacheFileFactory( context: Context diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt index 340f5536e..3cf6663f0 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt @@ -12,4 +12,11 @@ internal object JsonConverter { .setPrettyPrinting() .create() } + + val harInstance: Gson by lazy { + GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create() + } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt index 900f3379b..1b0f8ac2e 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt @@ -3,11 +3,18 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import com.chuckerteam.chucker.R import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.chuckerteam.chucker.internal.data.har.Har import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal object ShareUtils { + suspend fun harStringFromTransactions(transactions: List): String { + return withContext(Dispatchers.Default) { + JsonConverter.harInstance.toJson(Har.fromHttpTransactions(transactions)) + } + } + suspend fun getStringFromTransactions(transactions: List, context: Context): String { return withContext(Dispatchers.Default) { transactions.joinToString( 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 6555576f0..d86fca70b 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 @@ -80,6 +80,13 @@ internal class TransactionActivity : BaseChuckerActivity() { } ?: showToast(getString(R.string.chucker_request_not_ready)) true } + R.id.share_har -> { + viewModel.transaction.value?.let { + // TODO: +// share(ShareUtils.harStringFromTransactions(listOf(it))) + } ?: showToast(getString(R.string.chucker_request_not_ready)) + true + } else -> { super.onOptionsItemSelected(item) } 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 7a3dfc633..9718bc4db 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 @@ -138,7 +138,7 @@ internal class TransactionListFragment : if (transactions.isNullOrEmpty()) { Toast.makeText(requireContext(), R.string.chucker_export_empty_text, Toast.LENGTH_SHORT).show() } else { - val filecontent = ShareUtils.getStringFromTransactions(transactions, requireContext()) + val filecontent = ShareUtils.harStringFromTransactions(transactions) val file = viewModel.createExportFile(filecontent, cacheFileFactory) val uri = FileProvider.getUriForFile( requireContext(), diff --git a/library/src/main/res/menu/chucker_transaction.xml b/library/src/main/res/menu/chucker_transaction.xml index b7f632058..72bc7a2dc 100644 --- a/library/src/main/res/menu/chucker_transaction.xml +++ b/library/src/main/res/menu/chucker_transaction.xml @@ -27,6 +27,9 @@ + diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index 5648eed14..addfd1d66 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Nothing to export Share as text Share as curl command + Share as har file Save body to file Failed to open file chooser File saved successfully! From d31c7b8ca301efa5a0189c388f91d9351469fc36 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Thu, 6 Aug 2020 09:43:20 -0500 Subject: [PATCH 02/10] Update UI to export both txt and har. --- .../chucker/internal/data/har/Har.kt | 9 +++ .../chucker/internal/data/har/Request.kt | 4 +- .../chucker/internal/data/har/Response.kt | 8 +-- .../support/AndroidCacheFileFactory.kt | 3 +- .../internal/support/FileShareHelper.kt | 54 ++++++++++++++ .../chucker/internal/support/ShareUtils.kt | 7 -- .../chucker/internal/ui/MainViewModel.kt | 11 --- .../ui/transaction/TransactionActivity.kt | 13 +++- .../ui/transaction/TransactionListFragment.kt | 70 +++++++------------ .../res/menu/chucker_transactions_list.xml | 15 +++- .../chucker/internal/data/har/HarTest.kt | 10 +++ 11 files changed, 132 insertions(+), 72 deletions(-) create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt index 0ea1db097..082f13ec5 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt @@ -2,7 +2,10 @@ package com.chuckerteam.chucker.internal.data.har import com.chuckerteam.chucker.BuildConfig import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.chuckerteam.chucker.internal.support.JsonConverter import com.google.gson.annotations.SerializedName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.text.SimpleDateFormat import java.util.Locale @@ -15,6 +18,12 @@ internal data class Har( } companion object { + suspend fun harStringFromTransactions(transactions: List): String { + return withContext(Dispatchers.Default) { + JsonConverter.harInstance.toJson(fromHttpTransactions(transactions)) + } + } + fun fromHttpTransactions(transactions: List): Har { return Har( log = Log( diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt index 16b846ec8..1df6ace1c 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt @@ -23,8 +23,8 @@ internal data class Request( return Request( method = transaction.method!!, url = transaction.url!!, - httpVersion = transaction.protocol ?: "HTTP/1.1", // TODO: This is wrong! This is the response protocol. - cookies = emptyList(), // TODO: Grab from headers? + httpVersion = transaction.protocol ?: "HTTP/1.1", + cookies = emptyList(), headers = transaction.getParsedRequestHeaders()!!.map { Header(it.name, it.value) }, queryString = QueryString.fromUrl(HttpUrl.get(transaction.url!!)), postData = PostData.requestPostData(transaction), diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt index 7c8ce4817..1d04a366f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt @@ -23,14 +23,14 @@ internal data class Response( return Response( status = transaction.responseCode!!, statusText = transaction.responseMessage!!, - httpVersion = transaction.protocol ?: "HTTP/1.1", // TODO: This is actually unknown - cookies = emptyList(), // TODO: Grab this from headers? + httpVersion = transaction.protocol!!, + cookies = emptyList(), headers = transaction.getParsedResponseHeaders()!!.map { Header(it.name, it.value) }, content = PostData.responsePostData(transaction), - redirectUrl = "", // TODO: We could maybe get this off the response headers Location header? + redirectUrl = "", headerSize = transaction.responseHeaders!!.length, bodySize = transaction.responsePayloadSize ?: 0, - timings = Timings(0, 0, transaction.tookMs ?: 0) // TODO: We need more detailed values here! + timings = Timings(0, 0, transaction.tookMs ?: 0) ) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt index 028749e3f..8d149e4f4 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/AndroidCacheFileFactory.kt @@ -4,7 +4,8 @@ import android.content.Context import java.io.File import java.util.concurrent.atomic.AtomicLong -internal const val EXPORT_FILENAME = "transactions.har" +internal const val HAR_EXPORT_FILENAME = "transactions.har" +internal const val TXT_EXPORT_FILENAME = "transactions.txt" internal class AndroidCacheFileFactory( context: Context diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt new file mode 100644 index 000000000..6f9044e00 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt @@ -0,0 +1,54 @@ +package com.chuckerteam.chucker.internal.support + +import android.app.Activity +import android.content.ClipData +import android.content.Intent +import android.net.Uri +import androidx.core.app.ShareCompat +import androidx.core.content.FileProvider +import com.chuckerteam.chucker.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +internal class FileShareHelper( + private val activity: Activity, + private val exportFilename: String, + private val fileContentsFactory: suspend () -> String +) { + private val cacheFileFactory: FileFactory by lazy { + AndroidCacheFileFactory(activity) + } + + suspend fun share() { + val file = createExportFile(fileContentsFactory()) + val uri = FileProvider.getUriForFile( + activity, + activity.getString(R.string.chucker_provider_authority), + file + ) + shareFile(uri) + } + + private suspend fun createExportFile(content: String): File = withContext(Dispatchers.IO) { + val file = cacheFileFactory.create(exportFilename) + file.writeText(content) + return@withContext file + } + + private fun shareFile(uri: Uri) { + val sendIntent = ShareCompat.IntentBuilder.from(activity) + .setType(activity.contentResolver.getType(uri)) + .setChooserTitle(activity.getString(R.string.chucker_share_all_transactions_title)) + .setSubject(activity.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) + } + + activity.startActivity(Intent.createChooser(sendIntent, activity.getString(R.string.chucker_share_all_transactions_title))) + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt index 1b0f8ac2e..900f3379b 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/ShareUtils.kt @@ -3,18 +3,11 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import com.chuckerteam.chucker.R import com.chuckerteam.chucker.internal.data.entity.HttpTransaction -import com.chuckerteam.chucker.internal.data.har.Har import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal object ShareUtils { - suspend fun harStringFromTransactions(transactions: List): String { - return withContext(Dispatchers.Default) { - JsonConverter.harInstance.toJson(Har.fromHttpTransactions(transactions)) - } - } - suspend fun getStringFromTransactions(transactions: List, context: Context): String { return withContext(Dispatchers.Default) { transactions.joinToString( 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 02209c7b1..9ab2d76b8 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 @@ -10,13 +10,8 @@ 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File internal class MainViewModel : ViewModel() { @@ -43,12 +38,6 @@ internal class MainViewModel : ViewModel() { suspend fun getAllTransactions(): List? = 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 } 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 d86fca70b..b5220ec71 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 @@ -8,11 +8,16 @@ import android.view.MenuItem import androidx.activity.viewModels import androidx.core.app.ShareCompat import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.ViewPager import com.chuckerteam.chucker.R import com.chuckerteam.chucker.databinding.ChuckerActivityTransactionBinding +import com.chuckerteam.chucker.internal.data.har.Har +import com.chuckerteam.chucker.internal.support.FileShareHelper +import com.chuckerteam.chucker.internal.support.HAR_EXPORT_FILENAME import com.chuckerteam.chucker.internal.support.ShareUtils import com.chuckerteam.chucker.internal.ui.BaseChuckerActivity +import kotlinx.coroutines.launch internal class TransactionActivity : BaseChuckerActivity() { @@ -82,8 +87,12 @@ internal class TransactionActivity : BaseChuckerActivity() { } R.id.share_har -> { viewModel.transaction.value?.let { - // TODO: -// share(ShareUtils.harStringFromTransactions(listOf(it))) + val activity = this + lifecycleScope.launch { + FileShareHelper(activity, HAR_EXPORT_FILENAME) { + Har.harStringFromTransactions(listOf(it)) + }.share() + } } ?: showToast(getString(R.string.chucker_request_not_ready)) true } 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 9718bc4db..7731407de 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 @@ -1,8 +1,5 @@ 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 @@ -13,8 +10,6 @@ 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.fragment.app.viewModels import androidx.lifecycle.Observer @@ -22,10 +17,13 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import com.chuckerteam.chucker.R import com.chuckerteam.chucker.databinding.ChuckerFragmentTransactionListBinding +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.chuckerteam.chucker.internal.data.har.Har 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.FileShareHelper +import com.chuckerteam.chucker.internal.support.HAR_EXPORT_FILENAME import com.chuckerteam.chucker.internal.support.ShareUtils +import com.chuckerteam.chucker.internal.support.TXT_EXPORT_FILENAME import com.chuckerteam.chucker.internal.support.showDialog import com.chuckerteam.chucker.internal.ui.MainViewModel import kotlinx.coroutines.launch @@ -39,9 +37,6 @@ internal class TransactionListFragment : private lateinit var transactionsBinding: ChuckerFragmentTransactionListBinding private lateinit var transactionsAdapter: TransactionAdapter - private val cacheFileFactory: FileFactory by lazy { - AndroidCacheFileFactory(requireContext()) - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -105,14 +100,14 @@ internal class TransactionListFragment : ) true } - R.id.export -> { - requireContext().showDialog( - getExportDialogData(), - onPositiveClick = { - exportTransactions() - }, - onNegativeClick = null - ) + R.id.share_text -> { + performShareAction(TXT_EXPORT_FILENAME) { transactions -> + ShareUtils.getStringFromTransactions(transactions, requireContext()) + } + true + } + R.id.share_har -> { + performShareAction(HAR_EXPORT_FILENAME, Har.Companion::harStringFromTransactions) true } else -> { @@ -132,40 +127,29 @@ internal class TransactionListFragment : TransactionActivity.start(requireActivity(), transactionId) } - private fun exportTransactions() { + private fun performShareAction(filename: String, fileContentFactory: suspend (transactions: List) -> String) { + requireContext().showDialog( + getExportDialogData(), + onPositiveClick = { + exportTransactions(filename, fileContentFactory) + }, + onNegativeClick = null + ) + } + + private fun exportTransactions(filename: String, fileContentFactory: suspend (transactions: List) -> String) { lifecycleScope.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.harStringFromTransactions(transactions) - val file = viewModel.createExportFile(filecontent, cacheFileFactory) - val uri = FileProvider.getUriForFile( - requireContext(), - getString(R.string.chucker_provider_authority), - file - ) - shareFile(uri) + FileShareHelper(requireActivity(), filename) { + fileContentFactory(transactions) + }.share() } } } - 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), diff --git a/library/src/main/res/menu/chucker_transactions_list.xml b/library/src/main/res/menu/chucker_transactions_list.xml index 4e9a88bc7..e049631ec 100644 --- a/library/src/main/res/menu/chucker_transactions_list.xml +++ b/library/src/main/res/menu/chucker_transactions_list.xml @@ -6,10 +6,21 @@ android:icon="@drawable/chucker_ic_search_white" app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="androidx.appcompat.widget.SearchView" /> - + app:showAsAction="ifRoom" > + + + + + + + Date: Thu, 6 Aug 2020 17:34:59 -0500 Subject: [PATCH 03/10] Write some tests! --- .../chucker/internal/data/har/Har.kt | 10 +- .../chucker/internal/data/har/PostData.kt | 16 ++- .../chucker/internal/data/har/Request.kt | 6 +- .../chucker/internal/data/har/Response.kt | 6 +- .../chucker/internal/data/har/HarTest.kt | 103 +++++++++++++++++- 5 files changed, 122 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt index 082f13ec5..5bfdda9dd 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt @@ -1,5 +1,6 @@ package com.chuckerteam.chucker.internal.data.har +import androidx.annotation.VisibleForTesting import com.chuckerteam.chucker.BuildConfig import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.support.JsonConverter @@ -10,6 +11,7 @@ import java.text.SimpleDateFormat import java.util.Locale // http://www.softwareishard.com/blog/har-12-spec/ +// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md internal data class Har( @SerializedName("log") val log: Log ) { @@ -18,13 +20,11 @@ internal data class Har( } companion object { - suspend fun harStringFromTransactions(transactions: List): String { - return withContext(Dispatchers.Default) { - JsonConverter.harInstance.toJson(fromHttpTransactions(transactions)) - } + suspend fun harStringFromTransactions(transactions: List): String = withContext(Dispatchers.Default) { + JsonConverter.harInstance.toJson(fromHttpTransactions(transactions)) } - fun fromHttpTransactions(transactions: List): Har { + @VisibleForTesting fun fromHttpTransactions(transactions: List): Har { return Har( log = Log( version = "1.2", diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt index 077602a75..05616b3f0 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt @@ -4,23 +4,27 @@ import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.google.gson.annotations.SerializedName internal data class PostData( + @SerializedName("size") val size: Long, @SerializedName("mimeType") val mimeType: String, @SerializedName("text") val text: String ) { companion object { fun responsePostData(transaction: HttpTransaction): PostData? { - if (transaction.responseContentType == null || transaction.responseBody == null) return null + if (transaction.responsePayloadSize == null || !transaction.isResponseBodyPlainText) return null + return PostData( - mimeType = transaction.responseContentType!!, - text = transaction.responseBody!! + size = transaction.responsePayloadSize!!, + mimeType = transaction.responseContentType ?: "text", + text = transaction.responseBody ?: "" ) } fun requestPostData(transaction: HttpTransaction): PostData? { - if (transaction.requestContentType == null || transaction.requestBody == null) return null + if (transaction.requestPayloadSize == null || !transaction.isRequestBodyPlainText) return null return PostData( - mimeType = transaction.requestContentType!!, - text = transaction.requestBody!! + size = transaction.requestPayloadSize!!, + mimeType = transaction.requestContentType ?: "text", + text = transaction.requestBody ?: "" ) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt index 1df6ace1c..f08520583 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt @@ -12,7 +12,7 @@ internal data class Request( @SerializedName("headers") val headers: List
, @SerializedName("queryString") val queryString: List, @SerializedName("postData") val postData: PostData?, - @SerializedName("headerSize") val headerSize: Int, + @SerializedName("headersSize") val headersSize: Int, @SerializedName("bodySize") val bodySize: Long ) { companion object { @@ -25,10 +25,10 @@ internal data class Request( url = transaction.url!!, httpVersion = transaction.protocol ?: "HTTP/1.1", cookies = emptyList(), - headers = transaction.getParsedRequestHeaders()!!.map { Header(it.name, it.value) }, + headers = transaction.getParsedRequestHeaders()?.map { Header(it.name, it.value) } ?: emptyList(), queryString = QueryString.fromUrl(HttpUrl.get(transaction.url!!)), postData = PostData.requestPostData(transaction), - headerSize = transaction.requestHeaders!!.length, + headersSize = transaction.requestHeaders?.length ?: 0, bodySize = transaction.requestPayloadSize!! ) } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt index 1d04a366f..75a624d31 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt @@ -11,7 +11,7 @@ internal data class Response( @SerializedName("headers") val headers: List
, @SerializedName("content") val content: PostData?, @SerializedName("redirectURL") val redirectUrl: String, - @SerializedName("headerSize") val headerSize: Int, + @SerializedName("headersSize") val headersSize: Int, @SerializedName("bodySize") val bodySize: Long, @SerializedName("timings") val timings: Timings ) { @@ -25,10 +25,10 @@ internal data class Response( statusText = transaction.responseMessage!!, httpVersion = transaction.protocol!!, cookies = emptyList(), - headers = transaction.getParsedResponseHeaders()!!.map { Header(it.name, it.value) }, + headers = transaction.getParsedResponseHeaders()?.map { Header(it.name, it.value) } ?: emptyList(), content = PostData.responsePostData(transaction), redirectUrl = "", - headerSize = transaction.responseHeaders!!.length, + headersSize = transaction.responseHeaders?.length ?: 0, bodySize = transaction.responsePayloadSize ?: 0, timings = Timings(0, 0, transaction.tookMs ?: 0) ) diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt index 733aee54f..ba0c9b07d 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt @@ -1,10 +1,109 @@ package com.chuckerteam.chucker.internal.data.har +import com.chuckerteam.chucker.BuildConfig +import com.chuckerteam.chucker.TestTransactionFactory +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking import org.junit.Test +import java.util.Date class HarTest { - @Test - fun fromHttpTransactions_createsHarWithCorrectValues() { + @Test fun fromHttpTransactions_createsHarWithCorrectValues() { + val transaction = TestTransactionFactory.createTransaction("GET") + val har = Har.fromHttpTransactions(listOf(transaction)) + assertThat(har.log.version).isEqualTo("1.2") + assertThat(har.log.creator).isEqualTo(Creator("com.chuckerteam.chucker", BuildConfig.VERSION_NAME)) + assertThat(har.log.entries).hasSize(1) + val entry = har.log.entries[0] + assertThat(entry.startedDateTime).startsWith("1969-12-31T18:21:40.000") + assertThat(entry.time).isEqualTo(1000) + assertThat(entry.request).isEqualTo(Request( + method = "GET", + url = "http://localhost/getUsers", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + queryString = emptyList(), + postData = PostData(size = 1000, mimeType = "application/json", text = ""), + headersSize = 0, + bodySize = 1000 + )) + assertThat(entry.response).isEqualTo(Response( + status = 200, + statusText = "OK", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + content = PostData(size = 1000, mimeType = "application/json", text = """{"field": "value"}"""), + redirectUrl = "", + headersSize = 0, + bodySize = 1000, + timings = Timings(send = 0, wait = 0, receive = 1000) + )) + } + + @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { + val getTransaction = TestTransactionFactory.createTransaction("GET") + val postTransaction = TestTransactionFactory.createTransaction("POST") + val har = Har.fromHttpTransactions(listOf(getTransaction, postTransaction)) + assertThat(har.log.entries).hasSize(2) + assertThat(har.log.entries[0].request!!.method).isEqualTo("GET") + assertThat(har.log.entries[1].request!!.method).isEqualTo("POST") + } + @Test fun harString_createsJsonString(): Unit = runBlocking { + val transaction = TestTransactionFactory.createTransaction("GET") + assertThat(Har.harStringFromTransactions(listOf(transaction))).isEqualTo(""" + { + "log": { + "version": "1.2", + "creator": { + "name": "com.chuckerteam.chucker", + "version": "${BuildConfig.VERSION_NAME}" + }, + "entries": [ + { + "startedDateTime": "${Har.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", + "time": 1000, + "request": { + "method": "GET", + "url": "http://localhost/getUsers", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "queryString": [], + "postData": { + "size": 1000, + "mimeType": "application/json", + "text": "" + }, + "headersSize": 0, + "bodySize": 1000 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "content": { + "size": 1000, + "mimeType": "application/json", + "text": "{\"field\": \"value\"}" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 1000, + "timings": { + "send": 0, + "wait": 0, + "receive": 1000 + } + } + } + ] + } + } + """.trimIndent()) } } From 6ffcf70e9c91b8cbeac9dbc72917891e1b2deadf Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Fri, 7 Aug 2020 09:34:05 -0500 Subject: [PATCH 04/10] Fix pre commit checks. --- .../chucker/internal/data/har/Har.kt | 4 +- .../internal/support/FileShareHelper.kt | 7 +- .../ui/transaction/TransactionListFragment.kt | 10 +- .../chucker/internal/data/har/HarTest.kt | 155 ++++++++++-------- 4 files changed, 100 insertions(+), 76 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt index 5bfdda9dd..cf0e81c30 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt @@ -20,7 +20,9 @@ internal data class Har( } companion object { - suspend fun harStringFromTransactions(transactions: List): String = withContext(Dispatchers.Default) { + suspend fun harStringFromTransactions( + transactions: List + ): String = withContext(Dispatchers.Default) { JsonConverter.harInstance.toJson(fromHttpTransactions(transactions)) } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt index bdccbc068..e5ae08d3f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt @@ -49,6 +49,11 @@ internal class FileShareHelper( addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - activity.startActivity(Intent.createChooser(sendIntent, activity.getString(R.string.chucker_share_all_transactions_title))) + activity.startActivity( + Intent.createChooser( + sendIntent, + activity.getString(R.string.chucker_share_all_transactions_title) + ) + ) } } 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 7731407de..1ee0fbbc7 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 @@ -127,7 +127,10 @@ internal class TransactionListFragment : TransactionActivity.start(requireActivity(), transactionId) } - private fun performShareAction(filename: String, fileContentFactory: suspend (transactions: List) -> String) { + private fun performShareAction( + filename: String, + fileContentFactory: suspend (transactions: List) -> String + ) { requireContext().showDialog( getExportDialogData(), onPositiveClick = { @@ -137,7 +140,10 @@ internal class TransactionListFragment : ) } - private fun exportTransactions(filename: String, fileContentFactory: suspend (transactions: List) -> String) { + private fun exportTransactions( + filename: String, + fileContentFactory: suspend (transactions: List) -> String + ) { lifecycleScope.launch { val transactions = viewModel.getAllTransactions() if (transactions.isNullOrEmpty()) { diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt index ba0c9b07d..873ceda25 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt @@ -17,29 +17,38 @@ class HarTest { val entry = har.log.entries[0] assertThat(entry.startedDateTime).startsWith("1969-12-31T18:21:40.000") assertThat(entry.time).isEqualTo(1000) - assertThat(entry.request).isEqualTo(Request( - method = "GET", - url = "http://localhost/getUsers", - httpVersion = "HTTP", - cookies = emptyList(), - headers = emptyList(), - queryString = emptyList(), - postData = PostData(size = 1000, mimeType = "application/json", text = ""), - headersSize = 0, - bodySize = 1000 - )) - assertThat(entry.response).isEqualTo(Response( - status = 200, - statusText = "OK", - httpVersion = "HTTP", - cookies = emptyList(), - headers = emptyList(), - content = PostData(size = 1000, mimeType = "application/json", text = """{"field": "value"}"""), - redirectUrl = "", - headersSize = 0, - bodySize = 1000, - timings = Timings(send = 0, wait = 0, receive = 1000) - )) + assertThat(entry.request).isEqualTo( + Request( + method = "GET", + url = "http://localhost:80/getUsers", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + queryString = emptyList(), + postData = PostData(size = 1000, mimeType = "application/json", text = ""), + headersSize = 0, + bodySize = 1000 + ) + ) + assertThat(entry.response).isEqualTo( + Response( + status = 200, + statusText = "OK", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + content = PostData( + size = 1000, + mimeType = "application/json", + text = + """{"field": "value"}""" + ), + redirectUrl = "", + headersSize = 0, + bodySize = 1000, + timings = Timings(send = 0, wait = 0, receive = 1000) + ) + ) } @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { @@ -53,57 +62,59 @@ class HarTest { @Test fun harString_createsJsonString(): Unit = runBlocking { val transaction = TestTransactionFactory.createTransaction("GET") - assertThat(Har.harStringFromTransactions(listOf(transaction))).isEqualTo(""" - { - "log": { - "version": "1.2", - "creator": { - "name": "com.chuckerteam.chucker", - "version": "${BuildConfig.VERSION_NAME}" - }, - "entries": [ - { - "startedDateTime": "${Har.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", - "time": 1000, - "request": { - "method": "GET", - "url": "http://localhost/getUsers", - "httpVersion": "HTTP", - "cookies": [], - "headers": [], - "queryString": [], - "postData": { - "size": 1000, - "mimeType": "application/json", - "text": "" - }, - "headersSize": 0, - "bodySize": 1000 + assertThat(Har.harStringFromTransactions(listOf(transaction))).isEqualTo( + """ + { + "log": { + "version": "1.2", + "creator": { + "name": "com.chuckerteam.chucker", + "version": "${BuildConfig.VERSION_NAME}" }, - "response": { - "status": 200, - "statusText": "OK", - "httpVersion": "HTTP", - "cookies": [], - "headers": [], - "content": { - "size": 1000, - "mimeType": "application/json", - "text": "{\"field\": \"value\"}" - }, - "redirectURL": "", - "headersSize": 0, - "bodySize": 1000, - "timings": { - "send": 0, - "wait": 0, - "receive": 1000 + "entries": [ + { + "startedDateTime": "${Har.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", + "time": 1000, + "request": { + "method": "GET", + "url": "http://localhost:80/getUsers", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "queryString": [], + "postData": { + "size": 1000, + "mimeType": "application/json", + "text": "" + }, + "headersSize": 0, + "bodySize": 1000 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "content": { + "size": 1000, + "mimeType": "application/json", + "text": "{\"field\": \"value\"}" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 1000, + "timings": { + "send": 0, + "wait": 0, + "receive": 1000 + } + } } - } + ] } - ] - } - } - """.trimIndent()) + } + """.trimIndent() + ) } } From 41eed4af5d6dc8712526f5c864024c35858b3578 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Fri, 7 Aug 2020 09:41:02 -0500 Subject: [PATCH 05/10] Timezones are hard. --- .../java/com/chuckerteam/chucker/internal/data/har/HarTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt index 873ceda25..fed7d642c 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt @@ -15,7 +15,7 @@ class HarTest { assertThat(har.log.creator).isEqualTo(Creator("com.chuckerteam.chucker", BuildConfig.VERSION_NAME)) assertThat(har.log.entries).hasSize(1) val entry = har.log.entries[0] - assertThat(entry.startedDateTime).startsWith("1969-12-31T18:21:40.000") + assertThat(entry.startedDateTime).startsWith("1969-12-31T18:21") assertThat(entry.time).isEqualTo(1000) assertThat(entry.request).isEqualTo( Request( From 1785a7b54a4ec7c7ee5db96a025abad83ae2c434 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Fri, 7 Aug 2020 09:47:11 -0500 Subject: [PATCH 06/10] This is better than nothing. At least we're ensuring it can round trip to the same value. --- .../java/com/chuckerteam/chucker/internal/data/har/HarTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt index fed7d642c..123f7a290 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt @@ -15,7 +15,7 @@ class HarTest { assertThat(har.log.creator).isEqualTo(Creator("com.chuckerteam.chucker", BuildConfig.VERSION_NAME)) assertThat(har.log.entries).hasSize(1) val entry = har.log.entries[0] - assertThat(entry.startedDateTime).startsWith("1969-12-31T18:21") + assertThat(Har.DateFormat.get()!!.parse(entry.startedDateTime)).isEqualTo(Date(transaction.requestDate!!)) assertThat(entry.time).isEqualTo(1000) assertThat(entry.request).isEqualTo( Request( From 69bf8352eaf9af9b5082253a7dfd37067dc5ae13 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Mon, 24 Aug 2020 10:38:42 -0500 Subject: [PATCH 07/10] Address code review comments. --- .../chucker/internal/data/har/Entry.kt | 11 +- .../chucker/internal/data/har/Har.kt | 37 +----- .../chucker/internal/data/har/PostData.kt | 4 +- .../chucker/internal/data/har/QueryString.kt | 6 +- .../chucker/internal/data/har/Request.kt | 10 +- .../chucker/internal/data/har/Response.kt | 6 +- .../chucker/internal/support/HarUtils.kt | 34 +++++ .../chucker/internal/support/JsonConverter.kt | 2 +- .../ui/transaction/TransactionActivity.kt | 4 +- .../ui/transaction/TransactionListFragment.kt | 4 +- .../chucker/internal/data/har/EntryTest.kt | 67 ++++++++++ .../chucker/internal/data/har/HarTest.kt | 116 ++---------------- .../chucker/internal/data/har/PostDataTest.kt | 65 ++++++++++ .../internal/data/har/QueryStringTest.kt | 21 ++++ .../chucker/internal/data/har/RequestTest.kt | 42 +++++++ .../chucker/internal/data/har/ResponseTest.kt | 56 +++++++++ .../chucker/internal/support/HarUtilsTest.kt | 78 ++++++++++++ 17 files changed, 403 insertions(+), 160 deletions(-) create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/support/HarUtils.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/PostDataTest.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/QueryStringTest.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/RequestTest.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/data/har/ResponseTest.kt create mode 100644 library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt index d60269aaf..6c155abac 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt @@ -1,8 +1,11 @@ package com.chuckerteam.chucker.internal.data.har +import androidx.annotation.VisibleForTesting import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.google.gson.annotations.SerializedName +import java.text.SimpleDateFormat import java.util.Date +import java.util.Locale internal data class Entry( @SerializedName("startedDateTime") val startedDateTime: String, @@ -10,8 +13,12 @@ internal data class Entry( @SerializedName("request") val request: Request?, @SerializedName("response") val response: Response? ) { + @VisibleForTesting object DateFormat : ThreadLocal() { + override fun initialValue(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + } + companion object { - fun fromHttpTransaction(transaction: HttpTransaction): Entry = Entry( + fun fromHttpTransaction(transaction: HttpTransaction) = Entry( startedDateTime = transaction.requestDate.harFormatted(), time = transaction.tookMs ?: 0, request = Request.fromHttpTransaction(transaction), @@ -20,7 +27,7 @@ internal data class Entry( private fun Long?.harFormatted(): String { val date = if (this == null) Date() else Date(this) - return Har.DateFormat.get()!!.format(date) + return DateFormat.get()?.format(date) ?: "" } } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt index cf0e81c30..fcc252c85 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Har.kt @@ -1,42 +1,7 @@ package com.chuckerteam.chucker.internal.data.har -import androidx.annotation.VisibleForTesting -import com.chuckerteam.chucker.BuildConfig -import com.chuckerteam.chucker.internal.data.entity.HttpTransaction -import com.chuckerteam.chucker.internal.support.JsonConverter import com.google.gson.annotations.SerializedName -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.text.SimpleDateFormat -import java.util.Locale -// http://www.softwareishard.com/blog/har-12-spec/ -// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md internal data class Har( @SerializedName("log") val log: Log -) { - object DateFormat : ThreadLocal() { - override fun initialValue(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) - } - - companion object { - suspend fun harStringFromTransactions( - transactions: List - ): String = withContext(Dispatchers.Default) { - JsonConverter.harInstance.toJson(fromHttpTransactions(transactions)) - } - - @VisibleForTesting fun fromHttpTransactions(transactions: List): Har { - return Har( - log = Log( - version = "1.2", - creator = Creator( - name = BuildConfig.LIBRARY_PACKAGE_NAME, - version = BuildConfig.VERSION_NAME - ), - entries = transactions.map(Entry.Companion::fromHttpTransaction).filter { it.response != null } - ) - ) - } - } -} +) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt index 05616b3f0..4d7baecf4 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/PostData.kt @@ -13,7 +13,7 @@ internal data class PostData( if (transaction.responsePayloadSize == null || !transaction.isResponseBodyPlainText) return null return PostData( - size = transaction.responsePayloadSize!!, + size = transaction.responsePayloadSize ?: 0, mimeType = transaction.responseContentType ?: "text", text = transaction.responseBody ?: "" ) @@ -22,7 +22,7 @@ internal data class PostData( fun requestPostData(transaction: HttpTransaction): PostData? { if (transaction.requestPayloadSize == null || !transaction.isRequestBodyPlainText) return null return PostData( - size = transaction.requestPayloadSize!!, + size = transaction.requestPayloadSize ?: 0, mimeType = transaction.requestContentType ?: "text", text = transaction.requestBody ?: "" ) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt index 43cc4691b..b9c98aae0 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/QueryString.kt @@ -10,14 +10,12 @@ internal data class QueryString( companion object { fun fromUrl(url: HttpUrl): List { val querySize = url.querySize() - val list = ArrayList() - (0 until querySize).forEach { index -> - list += QueryString( + return (0 until querySize).map { index -> + QueryString( name = url.queryParameterName(index), value = url.queryParameterValue(index) ) } - return list } } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt index f08520583..02132c48d 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Request.kt @@ -21,15 +21,15 @@ internal data class Request( return null } return Request( - method = transaction.method!!, - url = transaction.url!!, - httpVersion = transaction.protocol ?: "HTTP/1.1", + method = transaction.method ?: "", + url = transaction.url ?: "", + httpVersion = transaction.protocol ?: "", cookies = emptyList(), headers = transaction.getParsedRequestHeaders()?.map { Header(it.name, it.value) } ?: emptyList(), - queryString = QueryString.fromUrl(HttpUrl.get(transaction.url!!)), + queryString = QueryString.fromUrl(HttpUrl.get(transaction.url ?: "")), postData = PostData.requestPostData(transaction), headersSize = transaction.requestHeaders?.length ?: 0, - bodySize = transaction.requestPayloadSize!! + bodySize = transaction.requestPayloadSize ?: 0 ) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt index 75a624d31..4b1df2a16 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Response.kt @@ -21,9 +21,9 @@ internal data class Response( return null } return Response( - status = transaction.responseCode!!, - statusText = transaction.responseMessage!!, - httpVersion = transaction.protocol!!, + status = transaction.responseCode ?: 0, + statusText = transaction.responseMessage ?: "", + httpVersion = transaction.protocol ?: "", cookies = emptyList(), headers = transaction.getParsedResponseHeaders()?.map { Header(it.name, it.value) } ?: emptyList(), content = PostData.responsePostData(transaction), diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/HarUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/HarUtils.kt new file mode 100644 index 000000000..fee0c9d64 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/HarUtils.kt @@ -0,0 +1,34 @@ +package com.chuckerteam.chucker.internal.support + +import androidx.annotation.VisibleForTesting +import com.chuckerteam.chucker.BuildConfig +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import com.chuckerteam.chucker.internal.data.har.Creator +import com.chuckerteam.chucker.internal.data.har.Entry +import com.chuckerteam.chucker.internal.data.har.Har +import com.chuckerteam.chucker.internal.data.har.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +// http://www.softwareishard.com/blog/har-12-spec/ +// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md +internal object HarUtils { + suspend fun harStringFromTransactions( + transactions: List + ): String = withContext(Dispatchers.Default) { + JsonConverter.nonNullSerializerInstance.toJson(fromHttpTransactions(transactions)) + } + + @VisibleForTesting fun fromHttpTransactions(transactions: List): Har { + return Har( + log = Log( + version = "1.2", + creator = Creator( + name = BuildConfig.LIBRARY_PACKAGE_NAME, + version = BuildConfig.VERSION_NAME + ), + entries = transactions.map(Entry.Companion::fromHttpTransaction).filter { it.response != null } + ) + ) + } +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt index 3cf6663f0..a72128827 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/JsonConverter.kt @@ -13,7 +13,7 @@ internal object JsonConverter { .create() } - val harInstance: Gson by lazy { + val nonNullSerializerInstance: Gson by lazy { GsonBuilder() .disableHtmlEscaping() .setPrettyPrinting() 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 b5220ec71..82d2edf03 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 @@ -12,9 +12,9 @@ import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.ViewPager import com.chuckerteam.chucker.R import com.chuckerteam.chucker.databinding.ChuckerActivityTransactionBinding -import com.chuckerteam.chucker.internal.data.har.Har import com.chuckerteam.chucker.internal.support.FileShareHelper import com.chuckerteam.chucker.internal.support.HAR_EXPORT_FILENAME +import com.chuckerteam.chucker.internal.support.HarUtils import com.chuckerteam.chucker.internal.support.ShareUtils import com.chuckerteam.chucker.internal.ui.BaseChuckerActivity import kotlinx.coroutines.launch @@ -90,7 +90,7 @@ internal class TransactionActivity : BaseChuckerActivity() { val activity = this lifecycleScope.launch { FileShareHelper(activity, HAR_EXPORT_FILENAME) { - Har.harStringFromTransactions(listOf(it)) + HarUtils.harStringFromTransactions(listOf(it)) }.share() } } ?: showToast(getString(R.string.chucker_request_not_ready)) 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 1ee0fbbc7..04820f1e8 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 @@ -18,10 +18,10 @@ import androidx.recyclerview.widget.DividerItemDecoration import com.chuckerteam.chucker.R import com.chuckerteam.chucker.databinding.ChuckerFragmentTransactionListBinding import com.chuckerteam.chucker.internal.data.entity.HttpTransaction -import com.chuckerteam.chucker.internal.data.har.Har import com.chuckerteam.chucker.internal.data.model.DialogData import com.chuckerteam.chucker.internal.support.FileShareHelper import com.chuckerteam.chucker.internal.support.HAR_EXPORT_FILENAME +import com.chuckerteam.chucker.internal.support.HarUtils import com.chuckerteam.chucker.internal.support.ShareUtils import com.chuckerteam.chucker.internal.support.TXT_EXPORT_FILENAME import com.chuckerteam.chucker.internal.support.showDialog @@ -107,7 +107,7 @@ internal class TransactionListFragment : true } R.id.share_har -> { - performShareAction(HAR_EXPORT_FILENAME, Har.Companion::harStringFromTransactions) + performShareAction(HAR_EXPORT_FILENAME, HarUtils::harStringFromTransactions) true } else -> { diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt new file mode 100644 index 000000000..4e20770a1 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt @@ -0,0 +1,67 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.TestTransactionFactory +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import java.util.Date + +internal class EntryTest { + @Test fun fromHttpTransaction_createsEntryWithCorrectStartedDateTime() { + val transaction = TestTransactionFactory.createTransaction("GET") + val entry = Entry.fromHttpTransaction(transaction) + + assertThat(Entry.DateFormat.get()!!.parse(entry.startedDateTime)).isEqualTo(Date(transaction.requestDate!!)) + assertThat(entry.time).isEqualTo(1000) + } + + @Test fun fromHttpTransaction_createsEntryWithCorrectTime() { + val transaction = TestTransactionFactory.createTransaction("GET") + val entry = Entry.fromHttpTransaction(transaction) + + assertThat(entry.time).isEqualTo(1000) + } + + @Test fun fromHttpTransaction_createsEntryWithCorrectRequest() { + val transaction = TestTransactionFactory.createTransaction("GET") + val entry = Entry.fromHttpTransaction(transaction) + + assertThat(entry.request).isEqualTo( + Request( + method = "GET", + url = "http://localhost:80/getUsers", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + queryString = emptyList(), + postData = PostData(size = 1000, mimeType = "application/json", text = ""), + headersSize = 0, + bodySize = 1000 + ) + ) + } + + @Test fun fromHttpTransaction_createsEntryWithCorrectResponse() { + val transaction = TestTransactionFactory.createTransaction("GET") + val entry = Entry.fromHttpTransaction(transaction) + + assertThat(entry.response).isEqualTo( + Response( + status = 200, + statusText = "OK", + httpVersion = "HTTP", + cookies = emptyList(), + headers = emptyList(), + content = PostData( + size = 1000, + mimeType = "application/json", + text = + """{"field": "value"}""" + ), + redirectUrl = "", + headersSize = 0, + bodySize = 1000, + timings = Timings(send = 0, wait = 0, receive = 1000) + ) + ) + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt index 123f7a290..a8e244441 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/HarTest.kt @@ -2,119 +2,29 @@ package com.chuckerteam.chucker.internal.data.har import com.chuckerteam.chucker.BuildConfig import com.chuckerteam.chucker.TestTransactionFactory +import com.chuckerteam.chucker.internal.support.HarUtils import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.runBlocking import org.junit.Test -import java.util.Date class HarTest { - @Test fun fromHttpTransactions_createsHarWithCorrectValues() { + @Test fun fromHttpTransactions_createsHarWithCorrectVersion() { val transaction = TestTransactionFactory.createTransaction("GET") - val har = Har.fromHttpTransactions(listOf(transaction)) + val har = HarUtils.fromHttpTransactions(listOf(transaction)) + assertThat(har.log.version).isEqualTo("1.2") - assertThat(har.log.creator).isEqualTo(Creator("com.chuckerteam.chucker", BuildConfig.VERSION_NAME)) - assertThat(har.log.entries).hasSize(1) - val entry = har.log.entries[0] - assertThat(Har.DateFormat.get()!!.parse(entry.startedDateTime)).isEqualTo(Date(transaction.requestDate!!)) - assertThat(entry.time).isEqualTo(1000) - assertThat(entry.request).isEqualTo( - Request( - method = "GET", - url = "http://localhost:80/getUsers", - httpVersion = "HTTP", - cookies = emptyList(), - headers = emptyList(), - queryString = emptyList(), - postData = PostData(size = 1000, mimeType = "application/json", text = ""), - headersSize = 0, - bodySize = 1000 - ) - ) - assertThat(entry.response).isEqualTo( - Response( - status = 200, - statusText = "OK", - httpVersion = "HTTP", - cookies = emptyList(), - headers = emptyList(), - content = PostData( - size = 1000, - mimeType = "application/json", - text = - """{"field": "value"}""" - ), - redirectUrl = "", - headersSize = 0, - bodySize = 1000, - timings = Timings(send = 0, wait = 0, receive = 1000) - ) - ) } - @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { - val getTransaction = TestTransactionFactory.createTransaction("GET") - val postTransaction = TestTransactionFactory.createTransaction("POST") - val har = Har.fromHttpTransactions(listOf(getTransaction, postTransaction)) - assertThat(har.log.entries).hasSize(2) - assertThat(har.log.entries[0].request!!.method).isEqualTo("GET") - assertThat(har.log.entries[1].request!!.method).isEqualTo("POST") + @Test fun fromHttpTransactions_createsHarWithCorrectCreator() { + val transaction = TestTransactionFactory.createTransaction("GET") + val har = HarUtils.fromHttpTransactions(listOf(transaction)) + + assertThat(har.log.creator).isEqualTo(Creator("com.chuckerteam.chucker", BuildConfig.VERSION_NAME)) } - @Test fun harString_createsJsonString(): Unit = runBlocking { + @Test fun fromHttpTransactions_createsHarWithCorrectEntries() { val transaction = TestTransactionFactory.createTransaction("GET") - assertThat(Har.harStringFromTransactions(listOf(transaction))).isEqualTo( - """ - { - "log": { - "version": "1.2", - "creator": { - "name": "com.chuckerteam.chucker", - "version": "${BuildConfig.VERSION_NAME}" - }, - "entries": [ - { - "startedDateTime": "${Har.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", - "time": 1000, - "request": { - "method": "GET", - "url": "http://localhost:80/getUsers", - "httpVersion": "HTTP", - "cookies": [], - "headers": [], - "queryString": [], - "postData": { - "size": 1000, - "mimeType": "application/json", - "text": "" - }, - "headersSize": 0, - "bodySize": 1000 - }, - "response": { - "status": 200, - "statusText": "OK", - "httpVersion": "HTTP", - "cookies": [], - "headers": [], - "content": { - "size": 1000, - "mimeType": "application/json", - "text": "{\"field\": \"value\"}" - }, - "redirectURL": "", - "headersSize": 0, - "bodySize": 1000, - "timings": { - "send": 0, - "wait": 0, - "receive": 1000 - } - } - } - ] - } - } - """.trimIndent() - ) + val har = HarUtils.fromHttpTransactions(listOf(transaction)) + + assertThat(har.log.entries).hasSize(1) } } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/PostDataTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/PostDataTest.kt new file mode 100644 index 000000000..d22155863 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/PostDataTest.kt @@ -0,0 +1,65 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.TestTransactionFactory +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +internal class PostDataTest { + @Test fun responsePostData_createPostDataWithCorrectSize() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.responsePostData(transaction) + + assertThat(postData?.size).isEqualTo(1000) + } + + @Test fun responsePostData_createPostDataWithCorrectMimeType() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.responsePostData(transaction) + + assertThat(postData?.mimeType).isEqualTo("application/json") + } + + @Test fun responsePostData_createPostDataWithCorrectText() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.responsePostData(transaction) + + assertThat(postData?.text).isEqualTo("""{"field": "value"}""") + } + + @Test fun responsePostData_returnsNullWhenPayloadSizeIsNull() { + val transaction = TestTransactionFactory.createTransaction("GET") + transaction.responsePayloadSize = null + val postData = PostData.responsePostData(transaction) + + assertThat(postData).isNull() + } + + @Test fun requestPostData_createPostDataWithCorrectSize() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.requestPostData(transaction) + + assertThat(postData?.size).isEqualTo(1000) + } + + @Test fun requestPostData_createPostDataWithCorrectMimeType() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.requestPostData(transaction) + + assertThat(postData?.mimeType).isEqualTo("application/json") + } + + @Test fun requestPostData_createPostDataWithCorrectText() { + val transaction = TestTransactionFactory.createTransaction("GET") + val postData = PostData.requestPostData(transaction) + + assertThat(postData?.text).isEqualTo("") + } + + @Test fun requestPostData_returnsNullWhenPayloadSizeIsNull() { + val transaction = TestTransactionFactory.createTransaction("GET") + transaction.requestPayloadSize = null + val postData = PostData.requestPostData(transaction) + + assertThat(postData).isNull() + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/QueryStringTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/QueryStringTest.kt new file mode 100644 index 000000000..38193ed92 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/QueryStringTest.kt @@ -0,0 +1,21 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.google.common.truth.Truth.assertThat +import okhttp3.HttpUrl +import org.junit.Test + +internal class QueryStringTest { + @Test fun fromUrl_createsCorrectListOfQueryStrings() { + val url = HttpUrl.get("https://fake.url.com/path?query1=a&query2=b#the-fragment-part") + val queryStringList = QueryString.fromUrl(url) + assertThat(queryStringList).hasSize(2) + assertThat(queryStringList[0]).isEqualTo(QueryString(name = "query1", value = "a")) + assertThat(queryStringList[1]).isEqualTo(QueryString(name = "query2", value = "b")) + } + + @Test fun fromUrl_createsCorrectEmptyListWhenNoQueryStringsPresent() { + val url = HttpUrl.get("https://fake.url.com/path#the-fragment-part") + val queryStringList = QueryString.fromUrl(url) + assertThat(queryStringList).isEmpty() + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/RequestTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/RequestTest.kt new file mode 100644 index 000000000..020124ba4 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/RequestTest.kt @@ -0,0 +1,42 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.TestTransactionFactory +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +internal class RequestTest { + @Test fun fromHttpTransaction_createsRequestWithCorrectMethod() { + val transaction = TestTransactionFactory.createTransaction("GET") + val request = Request.fromHttpTransaction(transaction) + + assertThat(request?.method).isEqualTo("GET") + } + + @Test fun fromHttpTransaction_createsRequestWithCorrectUrl() { + val transaction = TestTransactionFactory.createTransaction("GET") + val request = Request.fromHttpTransaction(transaction) + + assertThat(request?.url).isEqualTo("http://localhost:80/getUsers") + } + + @Test fun fromHttpTransaction_createsRequestWithCorrectHttpVersion() { + val transaction = TestTransactionFactory.createTransaction("GET") + val request = Request.fromHttpTransaction(transaction) + + assertThat(request?.httpVersion).isEqualTo("HTTP") + } + + @Test fun fromHttpTransaction_createsRequestWithCorrectPostData() { + val transaction = TestTransactionFactory.createTransaction("GET") + val request = Request.fromHttpTransaction(transaction) + + assertThat(request?.postData).isEqualTo(PostData(size = 1000, mimeType = "application/json", text = "")) + } + + @Test fun fromHttpTransaction_createsRequestWithCorrectBodySize() { + val transaction = TestTransactionFactory.createTransaction("GET") + val request = Request.fromHttpTransaction(transaction) + + assertThat(request?.bodySize).isEqualTo(1000) + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/ResponseTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/ResponseTest.kt new file mode 100644 index 000000000..f8e2949d3 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/ResponseTest.kt @@ -0,0 +1,56 @@ +package com.chuckerteam.chucker.internal.data.har + +import com.chuckerteam.chucker.TestTransactionFactory +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +internal class ResponseTest { + @Test fun fromHttpTransaction_createsResponseWithCorrectStatus() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.status).isEqualTo(200) + } + + @Test fun fromHttpTransaction_createsResponseWithCorrectStatusText() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.statusText).isEqualTo("OK") + } + + @Test fun fromHttpTransaction_createsResponseWithCorrectHttpVersion() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.httpVersion).isEqualTo("HTTP") + } + + @Test fun fromHttpTransaction_createsResponseWithCorrectContent() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.content).isEqualTo( + PostData( + size = 1000, + mimeType = "application/json", + text = + """{"field": "value"}""" + ) + ) + } + + @Test fun fromHttpTransaction_createsResponseWithCorrectBodySize() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.bodySize).isEqualTo(1000) + } + + @Test fun fromHttpTransaction_createsResponseWithCorrectTimings() { + val transaction = TestTransactionFactory.createTransaction("GET") + val response = Response.fromHttpTransaction(transaction) + + assertThat(response?.timings).isEqualTo(Timings(send = 0, wait = 0, receive = 1000)) + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt new file mode 100644 index 000000000..beee37107 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt @@ -0,0 +1,78 @@ +package com.chuckerteam.chucker.internal.support + +import com.chuckerteam.chucker.BuildConfig +import com.chuckerteam.chucker.TestTransactionFactory +import com.chuckerteam.chucker.internal.data.har.Entry +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.util.Date + +internal class HarUtilsTest { + @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { + val getTransaction = TestTransactionFactory.createTransaction("GET") + val postTransaction = TestTransactionFactory.createTransaction("POST") + val har = HarUtils.fromHttpTransactions(listOf(getTransaction, postTransaction)) + assertThat(har.log.entries).hasSize(2) + assertThat(har.log.entries[0].request!!.method).isEqualTo("GET") + assertThat(har.log.entries[1].request!!.method).isEqualTo("POST") + } + + @Test fun harString_createsJsonString(): Unit = runBlocking { + val transaction = TestTransactionFactory.createTransaction("GET") + assertThat(HarUtils.harStringFromTransactions(listOf(transaction))).isEqualTo( + """ + { + "log": { + "version": "1.2", + "creator": { + "name": "com.chuckerteam.chucker", + "version": "${BuildConfig.VERSION_NAME}" + }, + "entries": [ + { + "startedDateTime": "${Entry.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", + "time": 1000, + "request": { + "method": "GET", + "url": "http://localhost:80/getUsers", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "queryString": [], + "postData": { + "size": 1000, + "mimeType": "application/json", + "text": "" + }, + "headersSize": 0, + "bodySize": 1000 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP", + "cookies": [], + "headers": [], + "content": { + "size": 1000, + "mimeType": "application/json", + "text": "{\"field\": \"value\"}" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 1000, + "timings": { + "send": 0, + "wait": 0, + "receive": 1000 + } + } + } + ] + } + } + """.trimIndent() + ) + } +} From 5042a28da139458bf5b9c15d295dfa2cbde017a5 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Mon, 24 Aug 2020 15:41:34 -0500 Subject: [PATCH 08/10] Use java 8 time APIs. --- build.gradle | 1 + library/build.gradle | 6 ++++++ .../chucker/internal/data/har/Entry.kt | 20 ++++++++++--------- .../chucker/internal/data/har/EntryTest.kt | 5 +++-- .../chucker/internal/support/HarUtilsTest.kt | 5 +++-- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index fa27ac8ff..4e1e59e10 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { coroutineVersion = '1.3.8' // Google libraries + coreLibraryDesugaringVersion = '1.0.9' appCompatVersion = '1.1.0' constraintLayoutVersion = '1.1.3' materialComponentsVersion = '1.1.0' diff --git a/library/build.gradle b/library/build.gradle index 265468f3a..ec58a02e6 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,6 +8,9 @@ android { compileOptions { kotlinOptions.freeCompilerArgs += ['-module-name', "com.github.ChuckerTeam.Chucker.library"] + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { @@ -15,6 +18,7 @@ android { versionName VERSION_NAME versionCode VERSION_CODE.toInteger() consumerProguardFiles 'proguard-rules.pro' + multiDexEnabled true } kotlinOptions { @@ -59,6 +63,8 @@ dependencies { implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.squareup.okhttp3:okhttp:$okhttp3Version" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$coreLibraryDesugaringVersion" + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testImplementation "junit:junit:$vintageJunitVersion" diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt index 6c155abac..55286ea4d 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt @@ -1,10 +1,10 @@ package com.chuckerteam.chucker.internal.data.har -import androidx.annotation.VisibleForTesting import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.google.gson.annotations.SerializedName -import java.text.SimpleDateFormat -import java.util.Date +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Locale internal data class Entry( @@ -13,11 +13,9 @@ internal data class Entry( @SerializedName("request") val request: Request?, @SerializedName("response") val response: Response? ) { - @VisibleForTesting object DateFormat : ThreadLocal() { - override fun initialValue(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) - } - companion object { + val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + fun fromHttpTransaction(transaction: HttpTransaction) = Entry( startedDateTime = transaction.requestDate.harFormatted(), time = transaction.tookMs ?: 0, @@ -26,8 +24,12 @@ internal data class Entry( ) private fun Long?.harFormatted(): String { - val date = if (this == null) Date() else Date(this) - return DateFormat.get()?.format(date) ?: "" + val date = if (this == null) { + Instant.now().atZone(ZoneId.systemDefault()) + } else { + Instant.ofEpochMilli(this).atZone(ZoneId.systemDefault()) + } + return DATE_FORMAT.format(date) } } } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt index 4e20770a1..576d97d52 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt @@ -3,14 +3,15 @@ package com.chuckerteam.chucker.internal.data.har import com.chuckerteam.chucker.TestTransactionFactory import com.google.common.truth.Truth.assertThat import org.junit.Test -import java.util.Date +import java.time.Instant internal class EntryTest { @Test fun fromHttpTransaction_createsEntryWithCorrectStartedDateTime() { val transaction = TestTransactionFactory.createTransaction("GET") val entry = Entry.fromHttpTransaction(transaction) - assertThat(Entry.DateFormat.get()!!.parse(entry.startedDateTime)).isEqualTo(Date(transaction.requestDate!!)) + assertThat(Entry.DATE_FORMAT.parse(entry.startedDateTime, Instant::from)) + .isEqualTo(Instant.ofEpochMilli(transaction.requestDate!!)) assertThat(entry.time).isEqualTo(1000) } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt index beee37107..d6da2521a 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt @@ -6,7 +6,8 @@ import com.chuckerteam.chucker.internal.data.har.Entry import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import org.junit.Test -import java.util.Date +import java.time.Instant +import java.time.ZoneId internal class HarUtilsTest { @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { @@ -31,7 +32,7 @@ internal class HarUtilsTest { }, "entries": [ { - "startedDateTime": "${Entry.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", + "startedDateTime": "${Instant.ofEpochMilli(transaction.requestDate!!).atZone(ZoneId.systemDefault()).format(Entry.DATE_FORMAT)}", "time": 1000, "request": { "method": "GET", From 9c10cc85b0a96905027444994d04df4438d60f78 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Mon, 24 Aug 2020 15:53:04 -0500 Subject: [PATCH 09/10] Turn FileShareHelper into an object. --- .../internal/support/FileShareHelper.kt | 27 +++++++++---------- .../ui/transaction/TransactionActivity.kt | 4 +-- .../ui/transaction/TransactionListFragment.kt | 4 +-- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt index e5ae08d3f..450733b10 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/FileShareHelper.kt @@ -2,6 +2,7 @@ package com.chuckerteam.chucker.internal.support import android.app.Activity import android.content.ClipData +import android.content.Context import android.content.Intent import android.net.Uri import androidx.core.app.ShareCompat @@ -11,32 +12,28 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -internal class FileShareHelper( - private val activity: Activity, - private val exportFilename: String, - private val fileContentsFactory: suspend () -> String -) { - private val cacheFileFactory: FileFactory by lazy { - AndroidCacheFileFactory(activity) - } - - suspend fun share() { - val file = createExportFile(fileContentsFactory()) +internal object FileShareHelper { + suspend fun share(activity: Activity, exportFilename: String, fileContentsFactory: suspend () -> String) { + val file = createExportFile(activity.applicationContext, exportFilename, fileContentsFactory()) val uri = FileProvider.getUriForFile( activity, "${activity.packageName}.com.chuckerteam.chucker.provider", file ) - shareFile(uri) + shareFile(activity, uri) } - private suspend fun createExportFile(content: String): File = withContext(Dispatchers.IO) { - val file = cacheFileFactory.create(exportFilename) + private suspend fun createExportFile( + context: Context, + exportFilename: String, + content: String + ): File = withContext(Dispatchers.IO) { + val file = AndroidCacheFileFactory(context).create(exportFilename) file.writeText(content) return@withContext file } - private fun shareFile(uri: Uri) { + private fun shareFile(activity: Activity, uri: Uri) { val sendIntent = ShareCompat.IntentBuilder.from(activity) .setType(activity.contentResolver.getType(uri)) .setChooserTitle(activity.getString(R.string.chucker_share_all_transactions_title)) 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 82d2edf03..8d55432e0 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 @@ -89,9 +89,9 @@ internal class TransactionActivity : BaseChuckerActivity() { viewModel.transaction.value?.let { val activity = this lifecycleScope.launch { - FileShareHelper(activity, HAR_EXPORT_FILENAME) { + FileShareHelper.share(activity, HAR_EXPORT_FILENAME) { HarUtils.harStringFromTransactions(listOf(it)) - }.share() + } } } ?: showToast(getString(R.string.chucker_request_not_ready)) true 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 04820f1e8..a2256f6fc 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 @@ -149,9 +149,9 @@ internal class TransactionListFragment : if (transactions.isNullOrEmpty()) { Toast.makeText(requireContext(), R.string.chucker_export_empty_text, Toast.LENGTH_SHORT).show() } else { - FileShareHelper(requireActivity(), filename) { + FileShareHelper.share(requireActivity(), filename) { fileContentFactory(transactions) - }.share() + } } } } From 736ab4f42ecad8ac34bee3019cbf0b26c5a3a2bb Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Mon, 24 Aug 2020 16:36:53 -0500 Subject: [PATCH 10/10] Revert "Use java 8 time APIs." This reverts commit 5042a28da139458bf5b9c15d295dfa2cbde017a5. --- build.gradle | 1 - library/build.gradle | 6 ------ .../chucker/internal/data/har/Entry.kt | 20 +++++++++---------- .../chucker/internal/data/har/EntryTest.kt | 5 ++--- .../chucker/internal/support/HarUtilsTest.kt | 5 ++--- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index 3f8338d66..3d4ef42f3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,6 @@ buildscript { coroutineVersion = '1.3.9' // Google libraries - coreLibraryDesugaringVersion = '1.0.9' appCompatVersion = '1.2.0' constraintLayoutVersion = '2.0.0' materialComponentsVersion = '1.2.0' diff --git a/library/build.gradle b/library/build.gradle index 0da60f25a..1da696c04 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,9 +8,6 @@ android { compileOptions { kotlinOptions.freeCompilerArgs += ['-module-name', "com.github.ChuckerTeam.Chucker.library"] - coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { @@ -18,7 +15,6 @@ android { versionName VERSION_NAME versionCode VERSION_CODE.toInteger() consumerProguardFiles 'proguard-rules.pro' - multiDexEnabled true } kotlinOptions { @@ -66,8 +62,6 @@ dependencies { implementation "com.google.code.gson:gson:$gsonVersion" api "com.squareup.okhttp3:okhttp:$okhttp3Version" - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$coreLibraryDesugaringVersion" - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testImplementation "junit:junit:$vintageJunitVersion" diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt index 55286ea4d..6c155abac 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/data/har/Entry.kt @@ -1,10 +1,10 @@ package com.chuckerteam.chucker.internal.data.har +import androidx.annotation.VisibleForTesting import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.google.gson.annotations.SerializedName -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter +import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale internal data class Entry( @@ -13,9 +13,11 @@ internal data class Entry( @SerializedName("request") val request: Request?, @SerializedName("response") val response: Response? ) { - companion object { - val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + @VisibleForTesting object DateFormat : ThreadLocal() { + override fun initialValue(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + } + companion object { fun fromHttpTransaction(transaction: HttpTransaction) = Entry( startedDateTime = transaction.requestDate.harFormatted(), time = transaction.tookMs ?: 0, @@ -24,12 +26,8 @@ internal data class Entry( ) private fun Long?.harFormatted(): String { - val date = if (this == null) { - Instant.now().atZone(ZoneId.systemDefault()) - } else { - Instant.ofEpochMilli(this).atZone(ZoneId.systemDefault()) - } - return DATE_FORMAT.format(date) + val date = if (this == null) Date() else Date(this) + return DateFormat.get()?.format(date) ?: "" } } } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt index 576d97d52..4e20770a1 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/data/har/EntryTest.kt @@ -3,15 +3,14 @@ package com.chuckerteam.chucker.internal.data.har import com.chuckerteam.chucker.TestTransactionFactory import com.google.common.truth.Truth.assertThat import org.junit.Test -import java.time.Instant +import java.util.Date internal class EntryTest { @Test fun fromHttpTransaction_createsEntryWithCorrectStartedDateTime() { val transaction = TestTransactionFactory.createTransaction("GET") val entry = Entry.fromHttpTransaction(transaction) - assertThat(Entry.DATE_FORMAT.parse(entry.startedDateTime, Instant::from)) - .isEqualTo(Instant.ofEpochMilli(transaction.requestDate!!)) + assertThat(Entry.DateFormat.get()!!.parse(entry.startedDateTime)).isEqualTo(Date(transaction.requestDate!!)) assertThat(entry.time).isEqualTo(1000) } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt index d6da2521a..beee37107 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/HarUtilsTest.kt @@ -6,8 +6,7 @@ import com.chuckerteam.chucker.internal.data.har.Entry import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import org.junit.Test -import java.time.Instant -import java.time.ZoneId +import java.util.Date internal class HarUtilsTest { @Test fun fromHttpTransactions_createsHarWithMultipleEntries() { @@ -32,7 +31,7 @@ internal class HarUtilsTest { }, "entries": [ { - "startedDateTime": "${Instant.ofEpochMilli(transaction.requestDate!!).atZone(ZoneId.systemDefault()).format(Entry.DATE_FORMAT)}", + "startedDateTime": "${Entry.DateFormat.get()!!.format(Date(transaction.requestDate!!))}", "time": 1000, "request": { "method": "GET",