Add option to report errors via e-mail
This commit is contained in:
parent
9a21cd3ad7
commit
158c322cda
19 changed files with 308 additions and 40 deletions
14
README.md
14
README.md
|
|
@ -27,3 +27,17 @@ Click on the image for the demo video
|
|||
Here is another Secret Video QR code just for the curious minded
|
||||
|
||||

|
||||
|
||||
|
||||
## Note to self
|
||||
|
||||
When checking error messages, `jq` can be a life saver.
|
||||
|
||||
Example getting the stack trace for a specific error message:
|
||||
|
||||
`jq -r '.[].errors[] | select(.message == "Parsing Error") | .stacktrace' errors.json`
|
||||
|
||||
Or getting all htmls if an error happened in the json:
|
||||
`jq '.[] | select(.errors[]?.message == "Parsing Error") | .errors[].html'`
|
||||
|
||||
> Use -r when you want to avoid the JSON string quotes around output.
|
||||
|
|
@ -102,7 +102,8 @@ dependencies {
|
|||
|
||||
def glide_version = "4.15.1"
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
// kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.12.1'
|
||||
|
||||
def okhttp_version = "4.12.0"
|
||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,16 @@
|
|||
<service
|
||||
android:name=".ui.service.QueueService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -3,6 +3,7 @@ package org.fnives.tiktokdownloader.data.network
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fnives.tiktokdownloader.errortracking.ErrorTracer
|
||||
import org.fnives.tiktokdownloader.Logger
|
||||
import org.fnives.tiktokdownloader.data.model.VideoInPending
|
||||
import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile
|
||||
|
|
@ -28,6 +29,7 @@ class TikTokDownloadRemoteSource(
|
|||
withContext(Dispatchers.IO) {
|
||||
cookieStore.clear()
|
||||
wrapIntoProperException {
|
||||
ErrorTracer.startErrorTransaction(videoInPending.url)
|
||||
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
||||
val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url)
|
||||
val videoUrl: VideoFileUrl
|
||||
|
|
@ -43,7 +45,9 @@ class TikTokDownloadRemoteSource(
|
|||
}
|
||||
Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}")
|
||||
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
||||
try {
|
||||
val response = service.getVideo(videoUrl.videoFileUrl)
|
||||
ErrorTracer.cancelErrorTransaction()
|
||||
|
||||
VideoInSavingIntoFile(
|
||||
id = videoInPending.id,
|
||||
|
|
@ -56,6 +60,11 @@ class TikTokDownloadRemoteSource(
|
|||
},
|
||||
byteStream = response.videoInputStream
|
||||
)
|
||||
} catch (throwable: Throwable) {
|
||||
val exceptionName = (throwable as? HtmlException)?.exceptionName ?: "Unknown Error"
|
||||
ErrorTracer.addError("video-stream", "$exceptionName error while service.getVideo", throwable = throwable)
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,5 +85,7 @@ class TikTokDownloadRemoteSource(
|
|||
cause = throwable,
|
||||
html = (throwable as? HtmlException)?.html.orEmpty()
|
||||
)
|
||||
} finally {
|
||||
ErrorTracer.commitErrorTransaction()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,7 @@ package org.fnives.tiktokdownloader.data.network.exceptions
|
|||
class CaptchaRequiredException(
|
||||
message: String? = null, cause: Throwable? = null,
|
||||
override val html: String,
|
||||
) : Throwable(message, cause), HtmlException
|
||||
) : Throwable(message, cause), HtmlException {
|
||||
|
||||
override val exceptionName: String get() = "CaptchaRequired"
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ package org.fnives.tiktokdownloader.data.network.exceptions
|
|||
|
||||
interface HtmlException {
|
||||
val html: String
|
||||
val exceptionName: String
|
||||
}
|
||||
|
|
@ -3,4 +3,6 @@ package org.fnives.tiktokdownloader.data.network.exceptions
|
|||
class NetworkException(
|
||||
message: String? = null, cause: Throwable? = null,
|
||||
override val html: String,
|
||||
) : Throwable(message, cause), HtmlException
|
||||
) : Throwable(message, cause), HtmlException {
|
||||
override val exceptionName: String get() = "Network"
|
||||
}
|
||||
|
|
@ -5,4 +5,6 @@ import java.io.IOException
|
|||
class ParsingException(
|
||||
message: String? = null, cause: Throwable? = null,
|
||||
override val html: String,
|
||||
) : IOException(message, cause), HtmlException
|
||||
) : IOException(message, cause), HtmlException {
|
||||
override val exceptionName: String get() = "Parsing"
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package org.fnives.tiktokdownloader.data.network.exceptions
|
||||
|
||||
class VideoDeletedException(override val html: String) : Throwable(),
|
||||
HtmlException
|
||||
HtmlException {
|
||||
override val exceptionName: String get() = "Video Deleted"
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package org.fnives.tiktokdownloader.data.network.exceptions
|
||||
|
||||
class VideoPrivateException(override val html: String) : Throwable(),
|
||||
HtmlException
|
||||
HtmlException {
|
||||
override val exceptionName: String get() = "Video Private"
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package org.fnives.tiktokdownloader.data.network.parsing.converter
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import org.fnives.tiktokdownloader.errortracking.ErrorTracer
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.HtmlException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.VideoPrivateException
|
||||
import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPageUrl
|
||||
|
|
@ -28,7 +30,13 @@ class ActualVideoPageUrlConverter(
|
|||
.split("\"")[0]
|
||||
|
||||
ActualVideoPageUrl(actualVideoPageUrl, responseBodyAsString)
|
||||
} catch (_: Throwable) {
|
||||
} catch (throwable: Throwable) {
|
||||
val exceptionName = (throwable as? HtmlException)?.exceptionName ?: "Unknown Error"
|
||||
ErrorTracer.addError(
|
||||
html = responseBodyAsString,
|
||||
message = "$exceptionName in ActualVideoPageUrlConverter",
|
||||
throwable = throwable
|
||||
)
|
||||
ActualVideoPageUrl(null, responseBodyAsString)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package org.fnives.tiktokdownloader.data.network.parsing.converter
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import org.fnives.tiktokdownloader.errortracking.ErrorTracer
|
||||
import org.fnives.tiktokdownloader.Logger
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.HtmlException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException
|
||||
import org.fnives.tiktokdownloader.data.network.exceptions.VideoPrivateException
|
||||
|
|
@ -36,6 +38,7 @@ class VideoFileUrlConverter(
|
|||
VideoPrivateException::class,
|
||||
)
|
||||
private fun convert(responseBody: String): VideoFileUrl {
|
||||
try {
|
||||
val html = responseBody.also(throwIfIsCaptchaResponse::invoke)
|
||||
.also(throwIfVideoIsDeletedResponse::invoke)
|
||||
.also(throwIfVideoIsPrivateResponse::invoke)
|
||||
|
|
@ -45,6 +48,15 @@ class VideoFileUrlConverter(
|
|||
?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
|
||||
|
||||
return VideoFileUrl(url)
|
||||
} catch (throwable: Throwable) {
|
||||
val exceptionName = (throwable as? HtmlException)?.exceptionName ?: "Unknown Error"
|
||||
ErrorTracer.addError(
|
||||
html = responseBody,
|
||||
message = "$exceptionName in VideoFileUrlConverter",
|
||||
throwable = throwable
|
||||
)
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package org.fnives.tiktokdownloader.errortracking
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
object ErrorTracer {
|
||||
|
||||
private var errorTransaction: ErrorTransaction? = null
|
||||
private val errorTransactions = mutableListOf<ErrorTransaction>()
|
||||
private val errorListener = mutableListOf<() -> Unit>()
|
||||
|
||||
val hasErrors get() = errorTransactions.isNotEmpty()
|
||||
|
||||
fun startErrorTransaction(url: String) {
|
||||
errorTransaction = ErrorTransaction(url)
|
||||
}
|
||||
|
||||
fun addError(html: String, message: String, throwable: Throwable?) {
|
||||
errorTransaction?.addError(html = html, message = message, throwable = throwable)
|
||||
}
|
||||
|
||||
fun cancelErrorTransaction() {
|
||||
errorTransaction = null
|
||||
}
|
||||
|
||||
fun commitErrorTransaction() {
|
||||
val needsToNotify = !hasErrors
|
||||
val errorTransaction = errorTransaction
|
||||
if (errorTransaction != null) {
|
||||
errorTransactions.add(errorTransaction)
|
||||
}
|
||||
if (needsToNotify) {
|
||||
val errorListener = errorListener
|
||||
errorListener.forEach {
|
||||
doOnMainThread {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToHasErrorChanges(listener: () -> Unit): () -> Unit {
|
||||
doOnMainThread {
|
||||
errorListener.add(listener)
|
||||
}
|
||||
return fun() {
|
||||
doOnMainThread {
|
||||
errorListener.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doOnMainThread(action: () -> Unit) {
|
||||
Handler(Looper.getMainLooper()).post { action() }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getErrorAsJSON(): String {
|
||||
val errorTransactions = errorTransactions
|
||||
val gson = Gson()
|
||||
val arrayInner = errorTransactions.joinToString(",", transform = gson::toJson)
|
||||
return "[$arrayInner]"
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorTransaction(
|
||||
@SerializedName("url") val url: String
|
||||
) {
|
||||
@SerializedName("errors")
|
||||
val errors = mutableListOf<ErrorsDuringTransaction>()
|
||||
|
||||
@SerializedName("type")
|
||||
val errorType = "ErrorTransaction"
|
||||
|
||||
fun addError(html: String, message: String, throwable: Throwable?) {
|
||||
val stacktrace = throwable?.stackTraceToString()
|
||||
|
||||
errors.add(
|
||||
ErrorsDuringTransaction(
|
||||
html = html,
|
||||
message = message,
|
||||
stacktrace = stacktrace,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorsDuringTransaction(
|
||||
@SerializedName("html")
|
||||
val html: String,
|
||||
@SerializedName("message")
|
||||
val message: String,
|
||||
@SerializedName("stacktrace")
|
||||
val stacktrace: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.fnives.tiktokdownloader.errortracking
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
object SendErrorsAsEmail {
|
||||
|
||||
fun send(context: Context) {
|
||||
val subfolder = File(context.cacheDir, "errors")
|
||||
if (!subfolder.exists()) {
|
||||
subfolder.mkdirs()
|
||||
}
|
||||
|
||||
// Create the file inside subfolder
|
||||
val tempFile = File(subfolder, "errors.json")
|
||||
|
||||
// Write content
|
||||
tempFile.writeText(ErrorTracer.getErrorAsJSON())
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
tempFile
|
||||
)
|
||||
|
||||
val emailIntent = Intent(Intent.ACTION_SEND)
|
||||
emailIntent.setType("application/json")
|
||||
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf("projectsupport202@proton.me"))
|
||||
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Reporting Errors from TikTokDownloader")
|
||||
emailIntent.putExtra(Intent.EXTRA_TEXT, "Attached error as JSON")
|
||||
emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(Intent.createChooser(emailIntent, "Report errors in email..."))
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,15 @@ package org.fnives.tiktokdownloader.ui.main.settings
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.fnives.tiktokdownloader.errortracking.ErrorTracer
|
||||
import org.fnives.tiktokdownloader.R
|
||||
import org.fnives.tiktokdownloader.di.provideViewModels
|
||||
import org.fnives.tiktokdownloader.errortracking.SendErrorsAsEmail
|
||||
|
||||
class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
|
||||
|
|
@ -24,6 +30,38 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
|||
alwaysOpenAppHolder.setOnClickListener {
|
||||
viewModel.setAlwaysOpenApp(!alwaysOpenAppSwitch.isChecked)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(ErrorObserver())
|
||||
view.findViewById<View>(R.id.report_error_cta).setOnClickListener {
|
||||
SendErrorsAsEmail.send(it.context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class ErrorObserver : LifecycleEventObserver {
|
||||
private var subscription: (() -> Unit)? = null
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
val errorCTA = view?.findViewById<View>(R.id.report_error_cta)
|
||||
subscription = ErrorTracer.subscribeToHasErrorChanges {
|
||||
errorCTA?.isVisible = ErrorTracer.hasErrors
|
||||
}
|
||||
errorCTA?.isVisible = ErrorTracer.hasErrors
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_STOP -> {
|
||||
subscription?.invoke()
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
source.lifecycle.removeObserver(this)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
app:cardCornerRadius="@dimen/card_corner_radius"
|
||||
app:cardElevation="@dimen/card_elevation">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/always_open_app_holder"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -30,16 +35,28 @@
|
|||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/user_preference_always_open_app"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/always_open_app"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/report_error_cta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_height="wrap_content" />
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:minHeight="@dimen/minimum_touch_target"
|
||||
android:padding="@dimen/default_padding"
|
||||
android:text="@string/report_errors"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">24dp</dimen>
|
||||
<dimen name="minimum_touch_target">48dp</dimen>
|
||||
<dimen name="default_padding">16dp</dimen>
|
||||
<dimen name="medium_padding">8dp</dimen>
|
||||
<dimen name="card_elevation">2dp</dimen>
|
||||
|
|
|
|||
|
|
@ -51,4 +51,5 @@
|
|||
<string name="could_not_open">Couldn\'t open!</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="user_preference_always_open_app">Always open app when sharing video</string>
|
||||
<string name="report_errors">Report errors via e-mail</string>
|
||||
</resources>
|
||||
6
app/src/main/res/xml/file_paths.xml
Normal file
6
app/src/main/res/xml/file_paths.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path
|
||||
name="errors"
|
||||
path="errors/" />
|
||||
</paths>
|
||||
Loading…
Add table
Add a link
Reference in a new issue