diff --git a/README.md b/README.md
index 4b7af97..710bac6 100644
--- a/README.md
+++ b/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.
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 1df045c..b47df98 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 196f425..e4ff8f5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt
index 9e35990..2d9f385 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt
@@ -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,19 +45,26 @@ class TikTokDownloadRemoteSource(
}
Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}")
delay(delayBeforeRequest) // added just so captcha trigger may not happen
- val response = service.getVideo(videoUrl.videoFileUrl)
+ try {
+ val response = service.getVideo(videoUrl.videoFileUrl)
+ ErrorTracer.cancelErrorTransaction()
- VideoInSavingIntoFile(
- id = videoInPending.id,
- url = videoInPending.url,
- contentType = response.mediaType?.let {
- VideoInSavingIntoFile.ContentType(
- it.type,
- it.subtype
- )
- },
- byteStream = response.videoInputStream
- )
+ VideoInSavingIntoFile(
+ id = videoInPending.id,
+ url = videoInPending.url,
+ contentType = response.mediaType?.let {
+ VideoInSavingIntoFile.ContentType(
+ it.type,
+ it.subtype
+ )
+ },
+ 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()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt
index 735f0e8..bd6dfd2 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt
@@ -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
\ No newline at end of file
+) : Throwable(message, cause), HtmlException {
+
+ override val exceptionName: String get() = "CaptchaRequired"
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt
index 8e9546a..3d72896 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt
@@ -2,4 +2,5 @@ package org.fnives.tiktokdownloader.data.network.exceptions
interface HtmlException {
val html: String
+ val exceptionName: String
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt
index 2298c76..4134107 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt
@@ -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
\ No newline at end of file
+) : Throwable(message, cause), HtmlException {
+ override val exceptionName: String get() = "Network"
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt
index 94166be..785f875 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt
@@ -5,4 +5,6 @@ import java.io.IOException
class ParsingException(
message: String? = null, cause: Throwable? = null,
override val html: String,
-) : IOException(message, cause), HtmlException
\ No newline at end of file
+) : IOException(message, cause), HtmlException {
+ override val exceptionName: String get() = "Parsing"
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt
index ad510c8..953bef9 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt
@@ -1,4 +1,6 @@
package org.fnives.tiktokdownloader.data.network.exceptions
class VideoDeletedException(override val html: String) : Throwable(),
- HtmlException
\ No newline at end of file
+ HtmlException {
+ override val exceptionName: String get() = "Video Deleted"
+ }
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoPrivateException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoPrivateException.kt
index 073c5ef..d190bca 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoPrivateException.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoPrivateException.kt
@@ -1,4 +1,6 @@
package org.fnives.tiktokdownloader.data.network.exceptions
class VideoPrivateException(override val html: String) : Throwable(),
- HtmlException
\ No newline at end of file
+ HtmlException {
+ override val exceptionName: String get() = "Video Private"
+ }
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt
index 7d4afb3..6761158 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt
@@ -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)
}
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt
index 88858b7..6538f2b 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt
@@ -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,15 +38,25 @@ class VideoFileUrlConverter(
VideoPrivateException::class,
)
private fun convert(responseBody: String): VideoFileUrl {
- val html = responseBody.also(throwIfIsCaptchaResponse::invoke)
- .also(throwIfVideoIsDeletedResponse::invoke)
- .also(throwIfVideoIsPrivateResponse::invoke)
- val url =
- tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") }
- ?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") }
- ?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
+ try {
+ val html = responseBody.also(throwIfIsCaptchaResponse::invoke)
+ .also(throwIfVideoIsDeletedResponse::invoke)
+ .also(throwIfVideoIsPrivateResponse::invoke)
+ val url =
+ tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") }
+ ?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") }
+ ?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
- return VideoFileUrl(url)
+ 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 {
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/errortracking/ErrorTracer.kt b/app/src/main/java/org/fnives/tiktokdownloader/errortracking/ErrorTracer.kt
new file mode 100644
index 0000000..b8213e0
--- /dev/null
+++ b/app/src/main/java/org/fnives/tiktokdownloader/errortracking/ErrorTracer.kt
@@ -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()
+ 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()
+
+ @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?
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/errortracking/SendErrorsAsEmail.kt b/app/src/main/java/org/fnives/tiktokdownloader/errortracking/SendErrorsAsEmail.kt
new file mode 100644
index 0000000..c5aa187
--- /dev/null
+++ b/app/src/main/java/org/fnives/tiktokdownloader/errortracking/SendErrorsAsEmail.kt
@@ -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..."))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/settings/SettingsFragment.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/settings/SettingsFragment.kt
index e940cd1..9d5c4cf 100644
--- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/settings/SettingsFragment.kt
+++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/settings/SettingsFragment.kt
@@ -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(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(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 {
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index e5ca918..e40b2e6 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -20,26 +20,43 @@
app:cardElevation="@dimen/card_elevation">
+ android:orientation="vertical">
+
+
+
+
+
+
+
-
-
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 89047e7..95c3ac0 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,6 +1,7 @@
24dp
+ 48dp
16dp
8dp
2dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 85dfafb..6d12b48 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -51,4 +51,5 @@
Couldn\'t open!
Settings
Always open app when sharing video
+ Report errors via e-mail
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..45d3e92
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file