diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt index 7a6c8b113..583358cf7 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt @@ -38,6 +38,10 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa transactionDao.deleteAll() } + override suspend fun deleteSelectedTransactions(selectedTransactions: List) { + transactionDao.deleteSelected(selectedTransactions) + } + override suspend fun insertTransaction(transaction: HttpTransaction) { val id = transactionDao.insert(transaction) transaction.id = id ?: 0 @@ -57,4 +61,8 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa val timestamp = minTimestamp ?: 0L return transactionDao.getTransactionsInTimeRange(timestamp) } + + override suspend fun getSelectedTransactions(selectedTransactions: List): List { + return transactionDao.getSelectedTransactions(selectedTransactions) + } } diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt index d6eec28cf..8bc85f907 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionRepository.kt @@ -9,6 +9,7 @@ import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple * with [HttpTransaction] and [HttpTransactionTuple]. Please use [HttpTransactionDatabaseRepository] that * uses Room and SqLite to run those operations. */ +@Suppress("TooManyFunctions") internal interface HttpTransactionRepository { suspend fun insertTransaction(transaction: HttpTransaction) @@ -18,6 +19,8 @@ internal interface HttpTransactionRepository { suspend fun deleteAllTransactions() + suspend fun deleteSelectedTransactions(selectedTransactions: List) + fun getSortedTransactionTuples(): LiveData> fun getFilteredTransactionTuples( @@ -30,4 +33,6 @@ internal interface HttpTransactionRepository { suspend fun getAllTransactions(): List fun getTransactionsInTimeRange(minTimestamp: Long?): List + + suspend fun getSelectedTransactions(selectedTransactions: List): List } diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt index 909314211..c1e824674 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt @@ -9,6 +9,7 @@ import androidx.room.Update import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.data.entity.HttpTransactionTuple +@Suppress("TooManyFunctions") @Dao internal interface HttpTransactionDao { @Query( @@ -39,6 +40,9 @@ internal interface HttpTransactionDao { @Query("DELETE FROM transactions") suspend fun deleteAll(): Int + @Query("DELETE FROM transactions WHERE id IN (:selectedTransactions)") + suspend fun deleteSelected(selectedTransactions: List) + @Query("SELECT * FROM transactions WHERE id = :id") fun getById(id: Long): LiveData @@ -48,6 +52,9 @@ internal interface HttpTransactionDao { @Query("SELECT * FROM transactions") suspend fun getAll(): List + @Query("SELECT * FROM transactions WHERE id IN (:selectedTransactions)") + suspend fun getSelectedTransactions(selectedTransactions: List): List + @Query("SELECT * FROM transactions WHERE requestDate >= :timestamp") fun getTransactionsInTimeRange(timestamp: Long): List } diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainActivity.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainActivity.kt index 20530f023..e32188afa 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainActivity.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainActivity.kt @@ -46,6 +46,7 @@ import okio.Source import okio.buffer import okio.source +@Suppress("TooManyFunctions") internal class MainActivity : BaseChuckerActivity(), SearchView.OnQueryTextListener { @@ -53,6 +54,7 @@ internal class MainActivity : private lateinit var mainBinding: ChuckerActivityMainBinding private lateinit var transactionsAdapter: TransactionAdapter + private var isMultipleSelected = false private val applicationName: CharSequence get() = applicationInfo.loadLabel(packageManager) @@ -85,9 +87,19 @@ internal class MainActivity : mainBinding = ChuckerActivityMainBinding.inflate(layoutInflater) transactionsAdapter = - TransactionAdapter(this) { transactionId -> - TransactionActivity.start(this, transactionId) - } + TransactionAdapter( + context = this, + longPress = { transactionId -> + viewModel.selectItem(transactionId) + }, + onTransactionClick = { transactionId -> + if (isMultipleSelected) { + viewModel.selectItem(transactionId) + } else { + TransactionActivity.start(this, transactionId) + } + }, + ) with(mainBinding) { setContentView(root) @@ -117,6 +129,9 @@ internal class MainActivity : if (Chucker.showNotifications && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { handleNotificationsPermission() } + viewModel.isItemSelected.observe(this) { + isMultipleSelected = it + } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -171,6 +186,7 @@ internal class MainActivity : getClearDialogData(), onPositiveClick = { viewModel.clearTransactions() + resetSelection() }, onNegativeClick = null, ) @@ -178,34 +194,12 @@ internal class MainActivity : } R.id.share_text -> { - showDialog( - getExportDialogData(R.string.chucker_export_text_http_confirmation), - onPositiveClick = { - exportTransactions(EXPORT_TXT_FILE_NAME) { transactions -> - TransactionListDetailsSharable(transactions, encodeUrls = false) - } - }, - onNegativeClick = null, - ) + showShareTextDialog() true } R.id.share_har -> { - showDialog( - getExportDialogData(R.string.chucker_export_har_http_confirmation), - onPositiveClick = { - exportTransactions(EXPORT_HAR_FILE_NAME) { transactions -> - TransactionDetailsHarSharable( - HarUtils.harStringFromTransactions( - transactions, - getString(R.string.chucker_name), - getString(R.string.chucker_version), - ), - ) - } - }, - onNegativeClick = null, - ) + showShareHarDialog() true } @@ -225,6 +219,59 @@ internal class MainActivity : } } + private fun showShareHarDialog() { + showDialog( + getExportDialogData( + if (viewModel.isItemSelected.value == true) { + R.string.chucker_export_har_selected_http_confirmation + } else { + R.string.chucker_export_har_http_confirmation + }, + ), + onPositiveClick = { + exportTransactions(EXPORT_HAR_FILE_NAME) { transactions -> + TransactionDetailsHarSharable( + HarUtils.harStringFromTransactions( + transactions, + getString(R.string.chucker_name), + getString(R.string.chucker_version), + ), + ) + } + }, + onNegativeClick = null, + ) + } + + private fun showShareTextDialog() { + showDialog( + getExportDialogData( + if (viewModel.isItemSelected.value == true) { + R.string.chucker_export_text_selected_http_confirmation + } else { + R.string.chucker_export_text_http_confirmation + }, + ), + onPositiveClick = { + exportTransactions(EXPORT_TXT_FILE_NAME) { transactions -> + TransactionListDetailsSharable(transactions, encodeUrls = false) + } + }, + onNegativeClick = null, + ) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putIntArray("selectedItems", (transactionsAdapter.getSelectedItem().toIntArray())) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + val itemList = savedInstanceState.getIntArray("selectedItems") + transactionsAdapter.setSelectedItem(itemList?.toList() ?: emptyList()) + } + override fun onQueryTextSubmit(query: String): Boolean = true override fun onQueryTextChange(newText: String): Boolean { @@ -232,6 +279,10 @@ internal class MainActivity : return true } + private fun resetSelection() { + transactionsAdapter.clearSelections() + } + private fun exportTransactions( fileName: String, block: suspend (List) -> Sharable, @@ -258,7 +309,11 @@ internal class MainActivity : if (shareIntent != null) { startActivity(shareIntent) } else { - showToast(applicationContext.getString(R.string.chucker_export_no_file)) + showToast( + applicationContext.getString( + R.string.chucker_export_no_file, + ), + ) } } } @@ -266,7 +321,14 @@ internal class MainActivity : private fun getClearDialogData(): DialogData = DialogData( title = getString(R.string.chucker_clear), - message = getString(R.string.chucker_clear_http_confirmation), + message = + getString( + if (viewModel.isItemSelected.value == true) { + R.string.chucker_clear_selected_http_confirmation + } else { + R.string.chucker_clear_http_confirmation + }, + ), positiveButtonText = getString(R.string.chucker_clear), negativeButtonText = getString(R.string.chucker_cancel), ) diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainViewModel.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainViewModel.kt index 411353704..f961c0b53 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainViewModel.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/MainViewModel.kt @@ -4,6 +4,7 @@ import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import com.chuckerteam.chucker.internal.data.entity.HttpTransaction @@ -14,6 +15,11 @@ import kotlinx.coroutines.launch internal class MainViewModel : ViewModel() { private val currentFilter = MutableLiveData("") + private val selectedItemId: MutableLiveData> = + MutableLiveData>(mutableListOf()) + + private var _isItemSelected = MutableLiveData(false) + val isItemSelected = _isItemSelected.distinctUntilChanged() val transactions: LiveData> = currentFilter.switchMap { searchQuery -> @@ -32,7 +38,25 @@ internal class MainViewModel : ViewModel() { } } - suspend fun getAllTransactions(): List = RepositoryProvider.transaction().getAllTransactions() + fun selectItem(itemId: Long) { + viewModelScope.launch { + if (selectedItemId.value?.contains(itemId) == true) { + selectedItemId.value?.remove(itemId) + _isItemSelected.value = selectedItemId.value.isNullOrEmpty().not() == true + } else { + selectedItemId.value?.add(itemId) + _isItemSelected.value = true + } + } + } + + suspend fun getAllTransactions(): List { + return if (isItemSelected.value == true) { + RepositoryProvider.transaction().getSelectedTransactions(selectedItemId.value!!) + } else { + RepositoryProvider.transaction().getAllTransactions() + } + } fun updateItemsFilter(searchQuery: String) { currentFilter.value = searchQuery @@ -40,7 +64,12 @@ internal class MainViewModel : ViewModel() { fun clearTransactions() { viewModelScope.launch { - RepositoryProvider.transaction().deleteAllTransactions() + if (isItemSelected.value == true) { + _isItemSelected.value = false + RepositoryProvider.transaction().deleteSelectedTransactions(selectedItemId.value!!) + } else { + RepositoryProvider.transaction().deleteAllTransactions() + } } NotificationHelper.clearBuffer() } diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt index a49298d7a..85287797f 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionAdapter.kt @@ -3,6 +3,7 @@ package com.chuckerteam.chucker.internal.ui.transaction import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList +import android.util.TypedValue import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources @@ -22,9 +23,14 @@ import javax.net.ssl.HttpsURLConnection internal class TransactionAdapter internal constructor( context: Context, private val onTransactionClick: (Long) -> Unit, -) : ListAdapter( + private val longPress: (Long) -> Unit, +) : ListAdapter< + HttpTransactionTuple, + TransactionAdapter.TransactionViewHolder, + >( TransactionDiffCallback, ) { + private val selectedPos = mutableListOf() private val colorDefault: Int = ContextCompat.getColor(context, R.color.chucker_status_default) private val colorRequested: Int = ContextCompat.getColor( @@ -35,6 +41,32 @@ internal class TransactionAdapter internal constructor( private val color500: Int = ContextCompat.getColor(context, R.color.chucker_status_500) private val color400: Int = ContextCompat.getColor(context, R.color.chucker_status_400) private val color300: Int = ContextCompat.getColor(context, R.color.chucker_status_300) + private val chuckerStatusHighlighted: Int = ContextCompat.getColor(context, R.color.chucker_status_highlighted) + val outValue = TypedValue() + + @Suppress("UnusedPrivateProperty") + private val defaultColor = + context.theme.resolveAttribute( + android.R.attr.selectableItemBackground, + outValue, + true, + ) + + fun getSelectedItem(): List { + return selectedPos + } + + fun setSelectedItem(selectedItem: List) { + selectedPos.addAll(selectedItem) + } + + fun clearSelections() { + val pos = selectedPos + selectedPos.clear() + pos.forEach { + notifyItemRemoved(it) + } + } override fun onCreateViewHolder( parent: ViewGroup, @@ -63,14 +95,39 @@ internal class TransactionAdapter internal constructor( itemView.setOnClickListener { transactionId?.let { onTransactionClick.invoke(it) + if (selectedPos.isNotEmpty()) { + if (selectedPos.contains(adapterPosition)) { + selectedPos.remove(adapterPosition) + } else { + selectedPos.add(adapterPosition) + } + notifyItemChanged(adapterPosition) + } } } + + itemView.setOnLongClickListener { + transactionId?.let { + longPress.invoke(it) + if (selectedPos.contains(adapterPosition)) { + selectedPos.remove(adapterPosition) + } else { + selectedPos.add(adapterPosition) + } + notifyItemChanged(adapterPosition) + true + } ?: false + } } @SuppressLint("SetTextI18n") fun bind(transaction: HttpTransactionTuple) { transactionId = transaction.id - + if (selectedPos.find { it == adapterPosition } != null) { + itemView.setBackgroundColor(chuckerStatusHighlighted) + } else { + itemView.setBackgroundResource(outValue.resourceId) + } itemBinding.apply { displayGraphQlFields(transaction.graphQlOperationName, transaction.graphQlDetected) path.text = "${transaction.method} ${transaction.getFormattedPath(encode = false)}" @@ -134,6 +191,7 @@ private fun ChuckerListItemTransactionBinding.displayGraphQlFields( graphqlPath.isVisible = graphQLDetected if (graphQLDetected) { - graphqlPath.text = graphQlOperationName ?: root.resources.getString(R.string.chucker_graphql_operation_is_empty) + graphqlPath.text = graphQlOperationName + ?: root.resources.getString(R.string.chucker_graphql_operation_is_empty) } } diff --git a/library/src/main/res/values-night/colors.xml b/library/src/main/res/values-night/colors.xml index 19ce959f8..d07568932 100644 --- a/library/src/main/res/values-night/colors.xml +++ b/library/src/main/res/values-night/colors.xml @@ -22,6 +22,8 @@ #ffe082 #ef5350 + #FF9800 + #75bec4 #9e7162 #669f50 diff --git a/library/src/main/res/values/colors.xml b/library/src/main/res/values/colors.xml index 0a4bfa924..be29604cc 100644 --- a/library/src/main/res/values/colors.xml +++ b/library/src/main/res/values/colors.xml @@ -22,6 +22,8 @@ #ffffff00 #ffff0000 + #FF9800 + #F8FAFC #D2DADF #182531 diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index fcce5c79c..6d5993eed 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -50,8 +50,11 @@ Share transactions Transaction details All transactions - Do you want to clear complete network calls history? + Do you want to clear the complete network calls history? + Do you want to clear the selected network transactions? Do you want to export all network transactions as a text file? + Do you want to export selected network transactions as a text file? + Do you want to export selected network transactions as a .har file? Do you want to export all network transactions as a .har file? Do you want to save all network transactions as a text file? Do you want to save all network transactions as a .har file?