Skip to content

Commit

Permalink
[feat]: group crashes of the same app
Browse files Browse the repository at this point in the history
  • Loading branch information
F0x1d authored Oct 6, 2023
1 parent 39455d4 commit a8c9b2d
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 23 deletions.
16 changes: 9 additions & 7 deletions app/src/main/java/com/f0x1d/logfox/adapter/CrashesAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppCrash, ItemCrashBinding>(CRASH_DIFF) {
private val click: (AppCrashesCount) -> Unit,
private val delete: (AppCrashesCount) -> Unit
): BaseListAdapter<AppCrashesCount, ItemCrashBinding>(CRASH_DIFF) {

companion object {
private val CRASH_DIFF = object : DiffUtil.ItemCallback<AppCrash>() {
override fun areItemsTheSame(oldItem: AppCrash, newItem: AppCrash) = oldItem.id == newItem.id
private val CRASH_DIFF = object : DiffUtil.ItemCallback<AppCrashesCount>() {
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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ interface AppCrashDao {
@Query("SELECT * FROM AppCrash ORDER BY date_and_time DESC")
fun getAllAsFlow(): Flow<List<AppCrash>>

@Query("SELECT * FROM AppCrash WHERE package_name = :packageName")
suspend fun getAllByPackageName(packageName: String): List<AppCrash>

@Query("SELECT * FROM AppCrash WHERE id = :id")
fun get(id: Long): Flow<AppCrash?>

Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>("package_name")!!

@Provides
@ViewModelScoped
@AppName
fun provideAppName(savedStateHandle: SavedStateHandle) = savedStateHandle.get<String>("app_name")
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PackageName

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppName
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ fun Context.sendRecordingPausedNotification() = doIfPermitted {
)
}

fun Context.removeRecordingNotification() = notificationManagerCompat.cancel(RECORDING_NOTIFICATIONS_TAG, RECORDING_NOTIFICATIONS_ID)
fun Context.cancelRecordingNotification() = notificationManagerCompat.cancel(RECORDING_NOTIFICATIONS_TAG, RECORDING_NOTIFICATIONS_ID)
8 changes: 8 additions & 0 deletions app/src/main/java/com/f0x1d/logfox/model/AppCrashesCount.kt
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -120,7 +120,7 @@ class RecordingsRepository @Inject constructor(

fun end(recordingSaved: (LogRecording) -> Unit = {}) = runOnAppScope {
recordingStateFlow.update { RecordingState.SAVING }
context.removeRecordingNotification()
context.cancelRecordingNotification()

recordingJob?.cancel()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class MainActivity: BaseViewModelActivity<MainViewModel, ActivityMainBinding>(),
R.id.filtersFragment -> false
R.id.editFilterFragment -> false
R.id.chooseAppFragment -> false
R.id.appCrashesFragment -> false

else -> true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AppCrashesViewModel, FragmentAppCrashesBinding>() {

override val viewModel by viewModels<AppCrashesViewModel>()

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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -26,10 +26,18 @@ class CrashesFragment: BaseViewModelFragment<CrashesViewModel, FragmentCrashesBi
override val viewModel by viewModels<CrashesViewModel>()

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)
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppCrash, ItemCrashBinding>(binding) {
click: (AppCrashesCount) -> Unit,
delete: (AppCrashesCount) -> Unit
): BaseViewHolder<AppCrashesCount, ItemCrashBinding>(binding) {

init {
binding.root.setOnClickListener {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
31 changes: 31 additions & 0 deletions app/src/main/res/layout/fragment_app_crashes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScroll="true">

<com.f0x1d.logfox.ui.view.OpenSansToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/crashes"
app:titleCentered="true"
app:navigationIconTint="?colorOnSurface"
app:navigationIcon="@drawable/ic_arrow_back"/>
</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/crashes_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
Loading

0 comments on commit a8c9b2d

Please sign in to comment.