From d24de3d3654c221386eaa53dac0659bcc3603907 Mon Sep 17 00:00:00 2001 From: F0x1d Date: Fri, 6 Oct 2023 17:46:09 +0300 Subject: [PATCH] [feat]: group crashes of the same app --- .../f0x1d/logfox/adapter/CrashesAdapter.kt | 16 ++--- .../f0x1d/logfox/database/entity/AppCrash.kt | 6 ++ .../di/viewmodel/AppCrashesViewModelModule.kt | 32 ++++++++++ .../RecordingNotificationsExtensions.kt | 2 +- .../com/f0x1d/logfox/model/AppCrashesCount.kt | 8 +++ .../repository/logging/CrashesRepository.kt | 8 +++ .../logging/RecordingsRepository.kt | 4 +- .../f0x1d/logfox/ui/activity/MainActivity.kt | 1 + .../ui/fragment/crashes/AppCrashesFragment.kt | 61 +++++++++++++++++++ .../fragment/{ => crashes}/CrashesFragment.kt | 14 ++++- .../logfox/ui/viewholder/CrashViewHolder.kt | 22 ++++--- .../viewmodel/crashes/AppCrashesViewModel.kt | 41 +++++++++++++ .../viewmodel/crashes/CrashesViewModel.kt | 14 ++++- .../main/res/layout/fragment_app_crashes.xml | 31 ++++++++++ app/src/main/res/navigation/crashes.xml | 25 +++++++- 15 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/f0x1d/logfox/di/viewmodel/AppCrashesViewModelModule.kt create mode 100644 app/src/main/java/com/f0x1d/logfox/model/AppCrashesCount.kt create mode 100644 app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/AppCrashesFragment.kt rename app/src/main/java/com/f0x1d/logfox/ui/fragment/{ => crashes}/CrashesFragment.kt (82%) create mode 100644 app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/AppCrashesViewModel.kt create mode 100644 app/src/main/res/layout/fragment_app_crashes.xml diff --git a/app/src/main/java/com/f0x1d/logfox/adapter/CrashesAdapter.kt b/app/src/main/java/com/f0x1d/logfox/adapter/CrashesAdapter.kt index 47fe9a2d..fec86a37 100644 --- a/app/src/main/java/com/f0x1d/logfox/adapter/CrashesAdapter.kt +++ b/app/src/main/java/com/f0x1d/logfox/adapter/CrashesAdapter.kt @@ -4,20 +4,22 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import com.f0x1d.logfox.adapter.base.BaseListAdapter -import com.f0x1d.logfox.database.entity.AppCrash import com.f0x1d.logfox.databinding.ItemCrashBinding +import com.f0x1d.logfox.model.AppCrashesCount import com.f0x1d.logfox.ui.viewholder.CrashViewHolder class CrashesAdapter( - private val click: (AppCrash) -> Unit, - private val delete: (AppCrash) -> Unit -): BaseListAdapter(CRASH_DIFF) { + private val click: (AppCrashesCount) -> Unit, + private val delete: (AppCrashesCount) -> Unit +): BaseListAdapter(CRASH_DIFF) { companion object { - private val CRASH_DIFF = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AppCrash, newItem: AppCrash) = oldItem.id == newItem.id + private val CRASH_DIFF = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AppCrashesCount, newItem: AppCrashesCount) = + oldItem.lastCrash.id == newItem.lastCrash.id - override fun areContentsTheSame(oldItem: AppCrash, newItem: AppCrash) = oldItem == newItem + override fun areContentsTheSame(oldItem: AppCrashesCount, newItem: AppCrashesCount) = + oldItem == newItem } } diff --git a/app/src/main/java/com/f0x1d/logfox/database/entity/AppCrash.kt b/app/src/main/java/com/f0x1d/logfox/database/entity/AppCrash.kt index 11a37c2d..5621b5eb 100644 --- a/app/src/main/java/com/f0x1d/logfox/database/entity/AppCrash.kt +++ b/app/src/main/java/com/f0x1d/logfox/database/entity/AppCrash.kt @@ -21,6 +21,9 @@ interface AppCrashDao { @Query("SELECT * FROM AppCrash ORDER BY date_and_time DESC") fun getAllAsFlow(): Flow> + @Query("SELECT * FROM AppCrash WHERE package_name = :packageName") + suspend fun getAllByPackageName(packageName: String): List + @Query("SELECT * FROM AppCrash WHERE id = :id") fun get(id: Long): Flow @@ -33,6 +36,9 @@ interface AppCrashDao { @Delete suspend fun delete(appCrash: AppCrash) + @Query("DELETE FROM AppCrash WHERE package_name = :packageName") + suspend fun deleteByPackageName(packageName: String) + @Query("DELETE FROM AppCrash") suspend fun deleteAll() } diff --git a/app/src/main/java/com/f0x1d/logfox/di/viewmodel/AppCrashesViewModelModule.kt b/app/src/main/java/com/f0x1d/logfox/di/viewmodel/AppCrashesViewModelModule.kt new file mode 100644 index 00000000..d5f8933f --- /dev/null +++ b/app/src/main/java/com/f0x1d/logfox/di/viewmodel/AppCrashesViewModelModule.kt @@ -0,0 +1,32 @@ +package com.f0x1d.logfox.di.viewmodel + +import androidx.lifecycle.SavedStateHandle +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Qualifier + +@Module +@InstallIn(ViewModelComponent::class) +object AppCrashesViewModelModule { + + @Provides + @ViewModelScoped + @PackageName + fun providePackageName(savedStateHandle: SavedStateHandle) = savedStateHandle.get("package_name")!! + + @Provides + @ViewModelScoped + @AppName + fun provideAppName(savedStateHandle: SavedStateHandle) = savedStateHandle.get("app_name") +} + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class PackageName + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AppName \ No newline at end of file diff --git a/app/src/main/java/com/f0x1d/logfox/extensions/notifications/RecordingNotificationsExtensions.kt b/app/src/main/java/com/f0x1d/logfox/extensions/notifications/RecordingNotificationsExtensions.kt index 67cbb2c2..b9ef5156 100644 --- a/app/src/main/java/com/f0x1d/logfox/extensions/notifications/RecordingNotificationsExtensions.kt +++ b/app/src/main/java/com/f0x1d/logfox/extensions/notifications/RecordingNotificationsExtensions.kt @@ -59,4 +59,4 @@ fun Context.sendRecordingPausedNotification() = doIfPermitted { ) } -fun Context.removeRecordingNotification() = notificationManagerCompat.cancel(RECORDING_NOTIFICATIONS_TAG, RECORDING_NOTIFICATIONS_ID) \ No newline at end of file +fun Context.cancelRecordingNotification() = notificationManagerCompat.cancel(RECORDING_NOTIFICATIONS_TAG, RECORDING_NOTIFICATIONS_ID) \ No newline at end of file diff --git a/app/src/main/java/com/f0x1d/logfox/model/AppCrashesCount.kt b/app/src/main/java/com/f0x1d/logfox/model/AppCrashesCount.kt new file mode 100644 index 00000000..0f120311 --- /dev/null +++ b/app/src/main/java/com/f0x1d/logfox/model/AppCrashesCount.kt @@ -0,0 +1,8 @@ +package com.f0x1d.logfox.model + +import com.f0x1d.logfox.database.entity.AppCrash + +data class AppCrashesCount( + val lastCrash: AppCrash, + val count: Int = 1 +) \ No newline at end of file diff --git a/app/src/main/java/com/f0x1d/logfox/repository/logging/CrashesRepository.kt b/app/src/main/java/com/f0x1d/logfox/repository/logging/CrashesRepository.kt index a21767ca..e5bd411d 100644 --- a/app/src/main/java/com/f0x1d/logfox/repository/logging/CrashesRepository.kt +++ b/app/src/main/java/com/f0x1d/logfox/repository/logging/CrashesRepository.kt @@ -40,6 +40,14 @@ class CrashesRepository @Inject constructor( ANRDetector(crashCollected) ) + fun deleteAllByPackageName(appCrash: AppCrash) = runOnAppScope { + database.appCrashDao().getAllByPackageName(appCrash.packageName).forEach { + context.cancelCrashNotificationFor(it) + } + + database.appCrashDao().deleteByPackageName(appCrash.packageName) + } + override suspend fun updateInternal(item: AppCrash) = database.appCrashDao().update(item) override suspend fun deleteInternal(item: AppCrash) { diff --git a/app/src/main/java/com/f0x1d/logfox/repository/logging/RecordingsRepository.kt b/app/src/main/java/com/f0x1d/logfox/repository/logging/RecordingsRepository.kt index 76b0c652..1503d3e6 100644 --- a/app/src/main/java/com/f0x1d/logfox/repository/logging/RecordingsRepository.kt +++ b/app/src/main/java/com/f0x1d/logfox/repository/logging/RecordingsRepository.kt @@ -7,7 +7,7 @@ import com.f0x1d.logfox.database.entity.LogRecording import com.f0x1d.logfox.database.entity.UserFilter import com.f0x1d.logfox.extensions.exportFormatted import com.f0x1d.logfox.extensions.logline.filterAndSearch -import com.f0x1d.logfox.extensions.notifications.removeRecordingNotification +import com.f0x1d.logfox.extensions.notifications.cancelRecordingNotification import com.f0x1d.logfox.extensions.notifications.sendRecordingNotification import com.f0x1d.logfox.extensions.notifications.sendRecordingPausedNotification import com.f0x1d.logfox.model.LogLine @@ -120,7 +120,7 @@ class RecordingsRepository @Inject constructor( fun end(recordingSaved: (LogRecording) -> Unit = {}) = runOnAppScope { recordingStateFlow.update { RecordingState.SAVING } - context.removeRecordingNotification() + context.cancelRecordingNotification() recordingJob?.cancel() diff --git a/app/src/main/java/com/f0x1d/logfox/ui/activity/MainActivity.kt b/app/src/main/java/com/f0x1d/logfox/ui/activity/MainActivity.kt index bd30e4a0..2d4c4423 100644 --- a/app/src/main/java/com/f0x1d/logfox/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/f0x1d/logfox/ui/activity/MainActivity.kt @@ -96,6 +96,7 @@ class MainActivity: BaseViewModelActivity(), R.id.filtersFragment -> false R.id.editFilterFragment -> false R.id.chooseAppFragment -> false + R.id.appCrashesFragment -> false else -> true } diff --git a/app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/AppCrashesFragment.kt b/app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/AppCrashesFragment.kt new file mode 100644 index 00000000..bde2c403 --- /dev/null +++ b/app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/AppCrashesFragment.kt @@ -0,0 +1,61 @@ +package com.f0x1d.logfox.ui.fragment.crashes + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.f0x1d.logfox.adapter.CrashesAdapter +import com.f0x1d.logfox.databinding.FragmentAppCrashesBinding +import com.f0x1d.logfox.extensions.showAreYouSureDialog +import com.f0x1d.logfox.ui.fragment.base.BaseViewModelFragment +import com.f0x1d.logfox.utils.dpToPx +import com.f0x1d.logfox.viewmodel.crashes.AppCrashesViewModel +import com.google.android.material.divider.MaterialDividerItemDecoration +import dagger.hilt.android.AndroidEntryPoint +import dev.chrisbanes.insetter.applyInsetter + +@AndroidEntryPoint +class AppCrashesFragment: BaseViewModelFragment() { + + override val viewModel by viewModels() + + private val adapter = CrashesAdapter(click = { + findNavController().navigate( + AppCrashesFragmentDirections.actionAppCrashesFragmentToCrashDetailsActivity(it.lastCrash.id) + ) + }, delete = { + showAreYouSureDialog { + viewModel.deleteCrash(it.lastCrash) + } + }) + + override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentAppCrashesBinding.inflate(inflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.crashesRecycler.applyInsetter { + type(navigationBars = true) { + padding(vertical = true) + } + } + + binding.toolbar.title = viewModel.appName ?: viewModel.packageName + binding.toolbar.setNavigationOnClickListener { findNavController().popBackStack() } + + binding.crashesRecycler.layoutManager = LinearLayoutManager(requireContext()) + binding.crashesRecycler.addItemDecoration(MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL).apply { + dividerInsetStart = 80.dpToPx.toInt() + dividerInsetEnd = 10.dpToPx.toInt() + isLastItemDecorated = false + }) + binding.crashesRecycler.adapter = adapter + + viewModel.crashes.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/f0x1d/logfox/ui/fragment/CrashesFragment.kt b/app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/CrashesFragment.kt similarity index 82% rename from app/src/main/java/com/f0x1d/logfox/ui/fragment/CrashesFragment.kt rename to app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/CrashesFragment.kt index e0f1fb66..4289a8c2 100644 --- a/app/src/main/java/com/f0x1d/logfox/ui/fragment/CrashesFragment.kt +++ b/app/src/main/java/com/f0x1d/logfox/ui/fragment/crashes/CrashesFragment.kt @@ -1,4 +1,4 @@ -package com.f0x1d.logfox.ui.fragment +package com.f0x1d.logfox.ui.fragment.crashes import android.os.Bundle import android.view.LayoutInflater @@ -26,10 +26,18 @@ class CrashesFragment: BaseViewModelFragment() private val adapter = CrashesAdapter(click = { - findNavController().navigate(CrashesFragmentDirections.actionCrashesFragmentToCrashDetailsActivity(it.id)) + val direction = when (it.count) { + 1 -> CrashesFragmentDirections.actionCrashesFragmentToCrashDetailsActivity(it.lastCrash.id) + else -> CrashesFragmentDirections.actionCrashesFragmentToAppCrashesFragment( + packageName = it.lastCrash.packageName, + appName = it.lastCrash.appName + ) + } + + findNavController().navigate(direction) }, delete = { showAreYouSureDialog { - viewModel.deleteCrash(it) + viewModel.deleteCrashesByPackageName(it.lastCrash) } }) diff --git a/app/src/main/java/com/f0x1d/logfox/ui/viewholder/CrashViewHolder.kt b/app/src/main/java/com/f0x1d/logfox/ui/viewholder/CrashViewHolder.kt index f3cd1fed..e10da7a0 100644 --- a/app/src/main/java/com/f0x1d/logfox/ui/viewholder/CrashViewHolder.kt +++ b/app/src/main/java/com/f0x1d/logfox/ui/viewholder/CrashViewHolder.kt @@ -2,17 +2,18 @@ package com.f0x1d.logfox.ui.viewholder import android.annotation.SuppressLint import com.bumptech.glide.Glide -import com.f0x1d.logfox.database.entity.AppCrash +import com.f0x1d.logfox.R import com.f0x1d.logfox.databinding.ItemCrashBinding import com.f0x1d.logfox.extensions.loadIcon import com.f0x1d.logfox.extensions.toLocaleString +import com.f0x1d.logfox.model.AppCrashesCount import com.f0x1d.logfox.ui.viewholder.base.BaseViewHolder class CrashViewHolder( binding: ItemCrashBinding, - click: (AppCrash) -> Unit, - delete: (AppCrash) -> Unit -): BaseViewHolder(binding) { + click: (AppCrashesCount) -> Unit, + delete: (AppCrashesCount) -> Unit +): BaseViewHolder(binding) { init { binding.root.setOnClickListener { @@ -24,11 +25,16 @@ class CrashViewHolder( } @SuppressLint("SetTextI18n") - override fun bindTo(data: AppCrash) { - binding.icon.loadIcon(data.packageName) + override fun bindTo(data: AppCrashesCount) { + binding.icon.loadIcon(data.lastCrash.packageName) - binding.title.text = data.appName ?: data.packageName - binding.dateText.text = "${data.crashType.readableName} • ${data.dateAndTime.toLocaleString()}" + binding.title.text = data.lastCrash.appName ?: data.lastCrash.packageName + + binding.dateText.text = when (data.count) { + 1 -> "${data.lastCrash.crashType.readableName} • ${data.lastCrash.dateAndTime.toLocaleString()}" + + else -> "${binding.root.context.getString(R.string.crashes)}: ${data.count} • ${data.lastCrash.packageName}" + } } override fun recycle() = Glide.with(binding.icon).clear(binding.icon) diff --git a/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/AppCrashesViewModel.kt b/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/AppCrashesViewModel.kt new file mode 100644 index 00000000..c0ed9904 --- /dev/null +++ b/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/AppCrashesViewModel.kt @@ -0,0 +1,41 @@ +package com.f0x1d.logfox.viewmodel.crashes + +import android.app.Application +import androidx.lifecycle.asLiveData +import com.f0x1d.logfox.database.AppDatabase +import com.f0x1d.logfox.database.entity.AppCrash +import com.f0x1d.logfox.di.viewmodel.AppName +import com.f0x1d.logfox.di.viewmodel.PackageName +import com.f0x1d.logfox.model.AppCrashesCount +import com.f0x1d.logfox.repository.logging.CrashesRepository +import com.f0x1d.logfox.viewmodel.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class AppCrashesViewModel @Inject constructor( + @PackageName val packageName: String, + @AppName val appName: String?, + private val database: AppDatabase, + private val crashesRepository: CrashesRepository, + application: Application +): BaseViewModel(application) { + + val crashes = database.appCrashDao().getAllAsFlow() + .distinctUntilChanged() + .map { crashes -> + crashes.filter { crash -> + crash.packageName == packageName + }.map { + AppCrashesCount(it) + } + } + .flowOn(Dispatchers.IO) + .asLiveData() + + fun deleteCrash(appCrash: AppCrash) = crashesRepository.delete(appCrash) +} \ No newline at end of file diff --git a/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/CrashesViewModel.kt b/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/CrashesViewModel.kt index 7f33956c..5681133a 100644 --- a/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/CrashesViewModel.kt +++ b/app/src/main/java/com/f0x1d/logfox/viewmodel/crashes/CrashesViewModel.kt @@ -4,12 +4,14 @@ import android.app.Application import androidx.lifecycle.asLiveData import com.f0x1d.logfox.database.AppDatabase import com.f0x1d.logfox.database.entity.AppCrash +import com.f0x1d.logfox.model.AppCrashesCount import com.f0x1d.logfox.repository.logging.CrashesRepository import com.f0x1d.logfox.viewmodel.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import javax.inject.Inject @HiltViewModel @@ -21,10 +23,20 @@ class CrashesViewModel @Inject constructor( val crashes = database.appCrashDao().getAllAsFlow() .distinctUntilChanged() + .map { crashes -> + val groupedCrashes = crashes.groupBy { it.packageName } + + groupedCrashes.map { + AppCrashesCount( + lastCrash = it.value.first(), + count = it.value.size + ) + } + } .flowOn(Dispatchers.IO) .asLiveData() - fun deleteCrash(appCrash: AppCrash) = crashesRepository.delete(appCrash) + fun deleteCrashesByPackageName(appCrash: AppCrash) = crashesRepository.deleteAllByPackageName(appCrash) fun clearCrashes() = crashesRepository.clear() } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_app_crashes.xml b/app/src/main/res/layout/fragment_app_crashes.xml new file mode 100644 index 00000000..75051bdc --- /dev/null +++ b/app/src/main/res/layout/fragment_app_crashes.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/crashes.xml b/app/src/main/res/navigation/crashes.xml index b92d79c3..66ec4b84 100644 --- a/app/src/main/res/navigation/crashes.xml +++ b/app/src/main/res/navigation/crashes.xml @@ -6,11 +6,34 @@ + + + + + + +