Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3516 provide occurrence log #3517

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
1,057 changes: 1,057 additions & 0 deletions app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
<activity android:name=".components.announcements.AnnouncementsActivity" />
<activity android:name=".components.drafts.DraftsActivity" />
<activity android:name="com.keylesspalace.tusky.components.filters.EditFilterActivity" />
<activity android:name=".components.occurrence.OccurrenceActivity" />

<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
android:exported="false" />
Expand Down
51 changes: 38 additions & 13 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
import com.keylesspalace.tusky.components.occurrence.OccurrenceActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
import com.keylesspalace.tusky.components.search.SearchActivity
Expand Down Expand Up @@ -183,6 +184,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val activeAccount = accountManager.activeAccount
?: return // will be redirected to LoginActivity by BaseActivity

// TODO this works but seems a bit blunt for "intercept relevant log messages"?
// lifecycleScope.launch {
// Runtime.getRuntime().exec("logcat -c")
// Runtime.getRuntime().exec("logcat")
// .inputStream
// .bufferedReader()
// .useLines { lines -> lines.forEach {
// val x = it
// val y = 0
// }
// }
// }

var showNotificationTab = false
if (intent != null) {
/** there are two possibilities the accountId can be passed to MainActivity:
Expand Down Expand Up @@ -632,20 +646,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}

if (BuildConfig.DEBUG) {
// Add a "Developer tools" entry. Code that makes it easier to
// set the app state at runtime belongs here, it will never
// be exposed to users.
binding.mainDrawer.addItems(
DividerDrawerItem(),
secondaryDrawerItem {
nameText = "Developer tools"
isEnabled = true
iconicsIcon = GoogleMaterial.Icon.gmd_developer_mode
onClick = {
buildDeveloperToolsDialog().show()
binding.mainDrawer.apply {
addItems(
DividerDrawerItem(),
secondaryDrawerItem {
nameRes = R.string.action_occurrences
isEnabled = true
iconicsIcon = GoogleMaterial.Icon.gmd_event_note
onClick = {
startActivityWithSlideInAnimation(Intent(context, OccurrenceActivity::class.java))
}
},

// Add a "Developer tools" entry. Code that makes it easier to
// set the app state at runtime belongs here, it will never
// be exposed to users.
secondaryDrawerItem {
nameText = "Developer tools"
isEnabled = true
iconicsIcon = GoogleMaterial.Icon.gmd_developer_mode
onClick = {
buildDeveloperToolsDialog().show()
}
}
}
)
)
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.util.Log
import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.components.occurrence.OccurrenceRepository
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.SCHEMA_VERSION
Expand Down Expand Up @@ -50,6 +51,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var sharedPreferences: SharedPreferences

@Inject
lateinit var occurrenceRespository: OccurrenceRepository

override fun onCreate() {
// Uncomment me to get StrictMode violation logs
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Expand All @@ -63,6 +67,13 @@ class TuskyApplication : Application(), HasAndroidInjector {
// }
super.onCreate()

val existingUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
occurrenceRespository.handleException(e)

existingUncaughtHandler?.uncaughtException(t, e)
}

Security.insertProviderAt(Conscrypt.newProvider(), 1)

AutoDisposePlugins.setHideProxies(false) // a small performance optimization
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* Copyright Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */

package com.keylesspalace.tusky.components.occurrence

import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.io.IOException

class LogToDbInterceptor(private val occurrenceRespository: OccurrenceRepository) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val what = request.method + " " + request.url.toString()

val entityId = occurrenceRespository.handleApiCallStart(what)

val response: Response
try {
response = chain.proceed(request)
occurrenceRespository.handleApiCallFinish(entityId, response.code)
} catch (e: Exception) {
// TODO this case is used? If so add its message to the occurrence entity?
occurrenceRespository.handleApiCallFinish(entityId, 499)

throw e
}

return response
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/* Copyright Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/

package com.keylesspalace.tusky.components.occurrence

import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ActivityOccurrencesBinding
import com.keylesspalace.tusky.databinding.ItemOccurrenceBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.getDurationStringAllowMillis
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import java.text.DateFormat
import javax.inject.Inject

class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call it DebugActivity/DebugLogActivity or something like that

Copy link
Collaborator Author

@Lakoja Lakoja Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it's not used for debugging.

It should/could have the same name as in the ui (and there "debug" would be somewhat misleading).

It shows (hopefully) significant events in the app life. Therefore I was looking for a synonym of "event".


@Inject
lateinit var viewModelFactory: ViewModelFactory

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
Lakoja marked this conversation as resolved.
Show resolved Hide resolved

@Inject
lateinit var occurrenceRepository: OccurrenceRepository

@Inject
lateinit var db: AppDatabase

// private val viewModel: ListsViewModel by viewModels { viewModelFactory }

private val binding by viewBinding(ActivityOccurrencesBinding::inflate)

private val adapter = OccurrenceAdapter()

private var loading = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(binding.root)

setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply {
title = getString(R.string.title_occurrences)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}

binding.occurrenceList.adapter = adapter
binding.occurrenceList.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)

binding.swipeRefreshLayout.setOnRefreshListener { load() }
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)

// It's only function here so far: show "there is nothing"
binding.messageView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)

load()
}

private fun load() {
if (loading) {
return
}

lifecycleScope.launch {
binding.swipeRefreshLayout.isRefreshing = true
loading = true

val occurrences = occurrenceRepository.loadAll()

adapter.submitList(occurrences)

binding.messageView.visible(occurrences.isEmpty())
binding.occurrenceList.visible(occurrences.isNotEmpty())

binding.swipeRefreshLayout.isRefreshing = false
loading = false
}
}

override fun androidInjector() = dispatchingAndroidInjector

companion object {
fun newIntent(context: Context) = Intent(context, OccurrenceActivity::class.java)
}

private object OccurrenceDiffer : DiffUtil.ItemCallback<OccurrenceEntity>() {
override fun areItemsTheSame(oldItem: OccurrenceEntity, newItem: OccurrenceEntity): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: OccurrenceEntity, newItem: OccurrenceEntity): Boolean {
return oldItem == newItem
}
}

private inner class OccurrenceAdapter :
ListAdapter<OccurrenceEntity, BindingHolder<ItemOccurrenceBinding>>(OccurrenceDiffer) {

private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT)
private var lastAccount: AccountEntity? = null

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemOccurrenceBinding> {
return BindingHolder(ItemOccurrenceBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}

override fun onBindViewHolder(holder: BindingHolder<ItemOccurrenceBinding>, position: Int) {
val occurrence = getItem(position)

val defaultTextColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)

holder.binding.what.text = occurrence.what
holder.binding.what.setTextColor(
if (occurrence.type == OccurrenceEntity.Type.CRASH) {
Color.RED
} else {
defaultTextColor
}
)

holder.binding.code.text = occurrence.code?.toString() ?: ""
holder.binding.code.setTextColor(
if (occurrence.code != null && occurrence.code > 0) {
if (occurrence.code >= 400) {
baseContext.getColor(R.color.colorError)
} else if (occurrence.code >= 300) {
baseContext.getColor(R.color.colorWarning)
} else {
baseContext.getColor(R.color.colorSuccess)
}
} else {
defaultTextColor
}
)

holder.binding.whenDate.text =
getRelativeTimeSpanString([email protected], occurrence.startedAt.time, System.currentTimeMillis())
//dateFormat.format(occurrence.startedAt)
// TODO or AbsoluteTimeFormatter?

// TODO how does one get the current locale /and/or format numbers here?
val currentLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resources.configuration.locales[0]
} else {
resources.configuration.locale
}

var duration = ""
var durationMs = 0L
if (occurrence.finishedAt != null) {
durationMs = occurrence.finishedAt.time - occurrence.startedAt.time
duration = getDurationStringAllowMillis(currentLocale, durationMs)
}
holder.binding.duration.text = duration
holder.binding.duration.setTextColor(
if (durationMs >= 1000) {
baseContext.getColor(R.color.colorBad)
} else if (durationMs >= 400) {
baseContext.getColor(R.color.colorWarning)
} else {
baseContext.getColor(R.color.colorSuccess)
}
)

holder.binding.who.text = if (occurrence.accountId != null) {
val account = getAccount(occurrence.accountId)
account?.displayName ?: ""
} else {
""
}

holder.binding.trace.visible(occurrence.callTrace.isNotEmpty())
holder.binding.trace.text = OccurrenceEntity.reduceTrace(occurrence.callTrace)

// TODO cache some objects here? For example different helper objects (locale, number format, ...)
}

private fun getAccount(accountId: Long): AccountEntity? {
if (lastAccount?.id == accountId) {
return lastAccount
}

lastAccount = db.accountDao().get(accountId)

return lastAccount
}
}
}
Loading