From 14dc55433a4c7ddd41a2a087c3174393bccc84e5 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Sun, 21 Feb 2021 14:07:21 -0700 Subject: [PATCH] Use Timber for logging --- app/build.gradle | 6 ++ .../simplemarkdown/utility/ErrorHandler.kt | 2 +- .../simplemarkdown/utility/ReviewHelper.kt | 2 +- .../simplemarkdown/MarkdownApplication.kt | 13 ++++ .../simplemarkdown/utility/PersistentTree.kt | 75 +++++++++++++++++++ .../view/activity/SplashActivity.kt | 19 +++-- .../view/fragment/EditFragment.kt | 9 ++- .../view/fragment/MainFragment.kt | 69 ++++++++++++----- .../viewmodel/MarkdownViewModel.kt | 60 +++++++++++---- .../utility/CrashlyticsErrorHandler.kt | 4 +- .../simplemarkdown/utility/ReviewHelper.kt | 19 +++-- 11 files changed, 224 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/utility/PersistentTree.kt diff --git a/app/build.gradle b/app/build.gradle index a9b7741..3924d3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,6 +113,7 @@ dependencies { implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'com.commonsware.cwac:anddown:0.3.0' + implementation 'com.jakewharton.timber:timber:4.7.1' playImplementation 'com.android.billingclient:billing:3.0.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.firebase:firebase-crashlytics:17.3.1' @@ -178,3 +179,8 @@ fladle { ] projectId = 'simplemarkdown' } + +task pullLogFiles(type: Exec) { + commandLine 'adb', 'pull', + '/storage/emulated/0/Android/data/com.wbrawner.simplemarkdown/files/logs' +} \ No newline at end of file diff --git a/app/src/free/java/com/wbrawner/simplemarkdown/utility/ErrorHandler.kt b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ErrorHandler.kt index 3894dec..c1585ba 100644 --- a/app/src/free/java/com/wbrawner/simplemarkdown/utility/ErrorHandler.kt +++ b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ErrorHandler.kt @@ -16,7 +16,7 @@ class errorHandlerImpl { } override fun reportException(t: Throwable, message: String?) { - Log.e("ErrorHandler", "Caught non-fatal exception. Message: $message", t) + Timber.e(t, "Caught non-fatal exception. Message: $message") } } } diff --git a/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt index 86bc6f5..feb97ce 100644 --- a/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt +++ b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt @@ -6,6 +6,6 @@ import android.util.Log object ReviewHelper { // No review library for F-droid, so this is a no-op fun init(application: Application) { - Log.w("ReviewHelper", "ReviewHelper not enabled for free builds") + Timber.w("ReviewHelper", "ReviewHelper not enabled for free builds") } } \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt index f12e4de..a5e997e 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt @@ -2,7 +2,12 @@ package com.wbrawner.simplemarkdown import android.app.Application import android.os.StrictMode +import com.wbrawner.simplemarkdown.utility.PersistentTree import com.wbrawner.simplemarkdown.utility.ReviewHelper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File class MarkdownApplication : Application() { override fun onCreate() { @@ -15,6 +20,14 @@ class MarkdownApplication : Application() { .detectAll() .penaltyLog() .build()) + Timber.plant(Timber.DebugTree()) + GlobalScope.launch { + try { + Timber.plant(PersistentTree.create(File(getExternalFilesDir(null), "logs"))) + } catch (e: Exception) { + Timber.e(e, "Unable to create PersistentTree") + } + } } super.onCreate() ReviewHelper.init(this) diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/utility/PersistentTree.kt b/app/src/main/java/com/wbrawner/simplemarkdown/utility/PersistentTree.kt new file mode 100644 index 0000000..fa9750a --- /dev/null +++ b/app/src/main/java/com/wbrawner/simplemarkdown/utility/PersistentTree.kt @@ -0,0 +1,75 @@ +package com.wbrawner.simplemarkdown.utility + +import android.util.Log +import com.wbrawner.simplemarkdown.utility.PersistentTree.Companion.create +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.io.PrintStream +import java.text.SimpleDateFormat +import java.util.* + +/** + * A [Timber.Tree] implementation that persists all logs to disk for retrieval later. Create + * instances via [create] instead of calling the constructor directly. + */ +class PersistentTree private constructor(private val logFile: File) : Timber.Tree() { + private val dateFormat = object : ThreadLocal() { + override fun initialValue(): SimpleDateFormat = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + val timestamp = dateFormat.get()!!.format(System.currentTimeMillis()) + GlobalScope.launch(Dispatchers.IO) { + val priorityLetter = when (priority) { + Log.ASSERT -> "A" + Log.DEBUG -> "D" + Log.ERROR -> "E" + Log.INFO -> "I" + Log.VERBOSE -> "V" + Log.WARN -> "W" + else -> "U" + } + logFile.outputStream().use { stream -> + stream.bufferedWriter().use { + it.appendLine("$timestamp $priorityLetter/${tag ?: "SimpleMarkdown"}: $message") + } + t?.let { + PrintStream(stream).use { pStream -> + it.printStackTrace(pStream) + } + } + } + } + } + + init { + log(Log.INFO, "Persistent logging initialized, writing contents to ${logFile.absolutePath}") + } + + companion object { + /** + * Create a new instance of a [PersistentTree]. + * @param logDir A [File] pointing to a directory where the log files should be stored. Will be + * created if it doesn't exist. + * @throws IllegalArgumentException if [logDir] is a file instead of a directory + * @throws IOException if the directory does not exist or cannot be + * created/written to + */ + @Throws(IllegalArgumentException::class, IOException::class) + suspend fun create(logDir: File): PersistentTree = withContext(Dispatchers.IO) { + if (!logDir.mkdirs() && !logDir.isDirectory) + throw IllegalArgumentException("Unable to create log directory at ${logDir.absolutePath}") + val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(Date()) + val logFile = File(logDir, "persistent-log-$timestamp.log") + if (!logFile.createNewFile()) + throw IOException("Unable to create logFile at ${logFile.absolutePath}") + PersistentTree(logFile) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt index f49f697..8ebb565 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/activity/SplashActivity.kt @@ -41,12 +41,19 @@ class SplashActivity : AppCompatActivity() { AppCompatDelegate.setDefaultNightMode(darkMode) val uri = withContext(Dispatchers.IO) { - intent?.data - ?: PreferenceManager.getDefaultSharedPreferences(this@SplashActivity) - .getString(PREF_KEY_AUTOSAVE_URI, null) - ?.let { - Uri.parse(it) - } + intent?.data?.let { + Timber.d("Using uri from intent: $it") + it + } ?: PreferenceManager.getDefaultSharedPreferences(this@SplashActivity) + .getString(PREF_KEY_AUTOSAVE_URI, null) + ?.let { + Timber.d("Using uri from shared preferences: $it") + Uri.parse(it) + } + } + + if (uri == null) { + Timber.d("No intent provided to load data from") } val startIntent = Intent(this@SplashActivity, MainActivity::class.java) diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt index 81e7225..08eea33 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/EditFragment.kt @@ -7,7 +7,6 @@ import android.text.Editable import android.text.SpannableString import android.text.TextWatcher import android.text.style.BackgroundColorSpan -import android.util.Log import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -27,6 +26,7 @@ import com.wbrawner.simplemarkdown.utility.showKeyboard import com.wbrawner.simplemarkdown.view.ViewPagerPage import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel import kotlinx.coroutines.* +import timber.log.Timber import kotlin.math.abs class EditFragment : Fragment(), ViewPagerPage { @@ -153,13 +153,16 @@ class EditFragment : Fragment(), ViewPagerPage { var color = Color.TRANSPARENT if (sentence.syllableCount() > 25) color = Color.argb(100, 229, 232, 42) if (sentence.syllableCount() > 35) color = Color.argb(100, 193, 66, 66) - Log.d("SimpleMarkdown", "Sentence start: ${sentence.start()} end: ${sentence.end()}") + Timber.d("Sentence start: ${sentence.start()} end: ${ + sentence + .end() + }") span.setSpan(BackgroundColorSpan(color), sentence.start(), sentence.end(), 0) } markdownEditor?.setTextKeepState(span, TextView.BufferType.SPANNABLE) previousValue = searchFor val timeTakenMs = System.currentTimeMillis() - start - Log.d("SimpleMarkdown", "Handled markdown in $timeTakenMs ms") + Timber.d("Handled markdown in $timeTakenMs ms") } } diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt index f325183..7866fa9 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/view/fragment/MainFragment.kt @@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.ui.AppBarConfiguration @@ -31,7 +30,9 @@ import com.wbrawner.simplemarkdown.view.adapter.EditPagerAdapter import com.wbrawner.simplemarkdown.viewmodel.MarkdownViewModel import com.wbrawner.simplemarkdown.viewmodel.PREF_KEY_AUTOSAVE_URI import kotlinx.android.synthetic.main.fragment_main.* -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallback { @@ -81,7 +82,7 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba tabLayout!!.visibility = View.GONE } @Suppress("CAST_NEVER_SUCCEEDS") - viewModel.fileName.observe(viewLifecycleOwner, Observer { + viewModel.fileName.observe(viewLifecycleOwner, { toolbar?.title = it }) } @@ -108,10 +109,12 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba true } R.id.action_save_as -> { + Timber.d("Save as clicked") requestFileOp(REQUEST_SAVE_FILE) true } R.id.action_share -> { + Timber.d("Share clicked") val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.putExtra(Intent.EXTRA_TEXT, viewModel.markdownUpdates.value) shareIntent.type = "text/plain" @@ -122,14 +125,17 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba true } R.id.action_load -> { + Timber.d("Load clicked") requestFileOp(REQUEST_OPEN_FILE) true } R.id.action_new -> { + Timber.d("New clicked") promptSaveOrDiscardChanges() true } R.id.action_lock_swipe -> { + Timber.d("Lock swiping clicked") item.isChecked = !item.isChecked pager!!.setSwipeLocked(item.isChecked) true @@ -144,6 +150,7 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba withContext(Dispatchers.IO) { val enableErrorReports = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(getString(R.string.pref_key_error_reports_enabled), true) + Timber.d("MainFragment started. Error reports enabled? $enableErrorReports") errorHandler.enable(enableErrorReports) } } @@ -159,10 +166,13 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) - tabLayout!!.visibility = View.GONE - else - tabLayout!!.visibility = View.VISIBLE + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + Timber.d("Orientation changed to landscape, hiding tabs") + tabLayout?.visibility = View.GONE + } else { + Timber.d("Orientation changed to portrait, showing tabs") + tabLayout?.visibility = View.VISIBLE + } } override fun onRequestPermissionsResult( @@ -175,9 +185,11 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba // If request is cancelled, the result arrays are empty. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, open file save dialog + Timber.d("Storage permissions granted") requestFileOp(requestCode) } else { // Permission denied, do nothing + Timber.d("Storage permissions denied, unable to save or load files") context?.let { Toast.makeText(it, R.string.no_permissions, Toast.LENGTH_SHORT) .show() @@ -191,6 +203,11 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba when (requestCode) { REQUEST_OPEN_FILE -> { if (resultCode != Activity.RESULT_OK || data?.data == null) { + Timber.w( + "Unable to open file. Result ok? %b Intent uri: %s", + resultCode == Activity.RESULT_OK, + data?.data?.toString() + ) return } @@ -204,6 +221,10 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba .show() } } else { + Timber.d( + "File load succeeded, updating autosave uri in shared prefs: %s", + data.data.toString() + ) PreferenceManager.getDefaultSharedPreferences(requireContext()).edit { putString(PREF_KEY_AUTOSAVE_URI, data.data.toString()) } @@ -212,12 +233,16 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba } REQUEST_SAVE_FILE -> { if (resultCode != Activity.RESULT_OK || data?.data == null) { + Timber.w( + "Unable to save file. Result ok? %b Intent uri: %s", + resultCode == Activity.RESULT_OK, + data?.data?.toString() + ) return } lifecycleScope.launch { context?.let { - Log.d("SimpleMarkdown", "Saving file from onActivityResult") viewModel.save(it, data.data) } } @@ -229,22 +254,28 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba private fun promptSaveOrDiscardChanges() { if (!viewModel.shouldPromptSave()) { viewModel.reset("Untitled.md") + Timber.i("Removing autosave uri from shared prefs") PreferenceManager.getDefaultSharedPreferences(requireContext()).edit { remove(PREF_KEY_AUTOSAVE_URI) } return } - val context = context ?: return + val context = context ?: run { + Timber.w("Context is null, unable to show prompt for save or discard") + return + } AlertDialog.Builder(context) .setTitle(R.string.save_changes) .setMessage(R.string.prompt_save_changes) .setNegativeButton(R.string.action_discard) { _, _ -> + Timber.d("Discarding changes and deleting autosave uri from shared preferences") viewModel.reset("Untitled.md") PreferenceManager.getDefaultSharedPreferences(requireContext()).edit { remove(PREF_KEY_AUTOSAVE_URI) } } .setPositiveButton(R.string.action_save) { _, _ -> + Timber.d("Saving changes") requestFileOp(REQUEST_SAVE_FILE) } .create() @@ -252,25 +283,29 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba } private fun requestFileOp(requestType: Int) { - val context = context ?: return + val context = context ?: run { + Timber.w("File op requested but context was null, aborting") + return + } if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + Timber.i("Storage permission not granted, requesting") requestPermissions( arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), requestType ) return } - // If the user is going to save the file, we don't want to auto-save it for them - shouldAutoSave = false val intent = when (requestType) { REQUEST_SAVE_FILE -> { + Timber.d("Requesting save op") Intent(Intent.ACTION_CREATE_DOCUMENT).apply { type = "text/markdown" putExtra(Intent.EXTRA_TITLE, viewModel.fileName.value) } } REQUEST_OPEN_FILE -> { + Timber.d("Requesting open op") Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "*/*" if (MimeTypeMap.getSingleton().hasMimeType("md")) { @@ -280,7 +315,10 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba } } } - else -> null + else -> { + Timber.w("Ignoring unknown file op request: $requestType") + null + } } ?: return intent.addCategory(Intent.CATEGORY_OPENABLE) startActivityForResult( @@ -289,11 +327,6 @@ class MainFragment : Fragment(), ActivityCompat.OnRequestPermissionsResultCallba ) } - override fun onResume() { - super.onResume() - shouldAutoSave = true - } - companion object { // Request codes const val REQUEST_OPEN_FILE = 1 diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt b/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt index 987cd0d..76f8b05 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/viewmodel/MarkdownViewModel.kt @@ -3,15 +3,13 @@ package com.wbrawner.simplemarkdown.viewmodel import android.content.Context import android.content.SharedPreferences import android.net.Uri -import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.preference.PreferenceManager -import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.utility.getName import com.wbrawner.simplemarkdown.view.fragment.MainFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.Reader @@ -19,7 +17,7 @@ import java.util.concurrent.atomic.AtomicBoolean const val PREF_KEY_AUTOSAVE_URI = "autosave.uri" -class MarkdownViewModel : ViewModel() { +class MarkdownViewModel(val timber: Timber.Tree = Timber.asTree()) : ViewModel() { val fileName = MutableLiveData("Untitled.md") val markdownUpdates = MutableLiveData() val editorActions = MutableLiveData() @@ -32,7 +30,10 @@ class MarkdownViewModel : ViewModel() { } suspend fun load(context: Context, uri: Uri?): Boolean { - if (uri == null) return false + if (uri == null) { + timber.i("Ignoring call to load null uri") + return false + } return withContext(Dispatchers.IO) { try { context.contentResolver.openFileDescriptor(uri, "r")?.use { @@ -41,6 +42,7 @@ class MarkdownViewModel : ViewModel() { val content = fileInput.reader().use(Reader::readText) if (content.isBlank()) { // If we don't get anything back, then we can assume that reading the file failed + timber.i("Ignoring load for empty file $fileName from $fileInput") return@withContext false } isDirty.set(false) @@ -48,16 +50,31 @@ class MarkdownViewModel : ViewModel() { markdownUpdates.postValue(content) this@MarkdownViewModel.fileName.postValue(fileName) this@MarkdownViewModel.uri.postValue(uri) + timber.i("Loaded file $fileName from $fileInput") + timber.v("File contents:\n$content") true - } ?: false - } catch (ignored: Exception) { + } ?: run { + timber.w("Open file descriptor returned null for uri: $uri") + false + } + } catch (e: Exception) { + timber.e(e, "Failed to open file descriptor for uri: $uri") false } } } - suspend fun save(context: Context, givenUri: Uri? = this.uri.value): Boolean { - val uri = givenUri ?: this.uri.value ?: return false + suspend fun save(context: Context, givenUri: Uri? = null): Boolean { + val uri = givenUri?.let { + timber.i("Saving file with given uri: $uri") + it + } ?: this.uri.value?.let { + timber.i("Saving file with cached uri: $uri") + it + } ?: run { + timber.w("Save called with no uri") + return@save false + } return withContext(Dispatchers.IO) { try { val fileName = uri.getName(context) @@ -66,11 +83,17 @@ class MarkdownViewModel : ViewModel() { ?.use { it.write(markdownUpdates.value ?: "") } - ?: return@withContext false + ?: run { + timber.w("Open output stream returned null for uri: $uri") + return@withContext false + } this@MarkdownViewModel.fileName.postValue(fileName) this@MarkdownViewModel.uri.postValue(uri) + isDirty.set(false) + timber.i("Saved file $fileName to uri $uri") true - } catch (ignored: Exception) { + } catch (e: Exception) { + timber.e(e, "Failed to save file at uri: $uri") false } } @@ -78,31 +101,38 @@ class MarkdownViewModel : ViewModel() { suspend fun autosave(context: Context, sharedPrefs: SharedPreferences) { val isAutoSaveEnabled = sharedPrefs.getBoolean(MainFragment.KEY_AUTOSAVE, true) + timber.d("Autosave called. isEnabled? $isAutoSaveEnabled") if (!isDirty.get() || !isAutoSaveEnabled) { + timber.i("Ignoring call to autosave. Contents haven't changed or autosave not enabled") return } val uri = if (save(context)) { - Log.d("SimpleMarkdown", "Saving file from onPause") + timber.i("Autosave with cached uri succeeded: ${uri.value}") uri.value } else { // The user has left the app, with autosave enabled, and we don't already have a // Uri for them or for some reason we were unable to save to the original Uri. In // this case, we need to just save to internal file storage so that we can recover - val fileUri = Uri.fromFile(File(context.filesDir, fileName.value?: "Untitled.md")) - Log.d("SimpleMarkdown", "Saving file from onPause failed, trying again") + val fileUri = Uri.fromFile(File(context.filesDir, fileName.value ?: "Untitled.md")) + timber.i("No cached uri for autosave, saving to $fileUri instead") if (save(context, fileUri)) { fileUri } else { null } - } ?: return + } ?: run { + timber.w("Unable to perform autosave, uri was null") + return@autosave + } + timber.i("Persisting autosave uri in shared prefs: $uri") sharedPrefs.edit() .putString(PREF_KEY_AUTOSAVE_URI, uri.toString()) .apply() } fun reset(untitledFileName: String) { + timber.i("Resetting view model to default state") fileName.postValue(untitledFileName) uri.postValue(null) markdownUpdates.postValue("") diff --git a/app/src/play/java/com/wbrawner/simplemarkdown/utility/CrashlyticsErrorHandler.kt b/app/src/play/java/com/wbrawner/simplemarkdown/utility/CrashlyticsErrorHandler.kt index cfc8d81..bf4c065 100644 --- a/app/src/play/java/com/wbrawner/simplemarkdown/utility/CrashlyticsErrorHandler.kt +++ b/app/src/play/java/com/wbrawner/simplemarkdown/utility/CrashlyticsErrorHandler.kt @@ -1,8 +1,8 @@ package com.wbrawner.simplemarkdown.utility -import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics import com.wbrawner.simplemarkdown.BuildConfig +import timber.log.Timber import kotlin.reflect.KProperty class CrashlyticsErrorHandler : ErrorHandler { @@ -15,7 +15,7 @@ class CrashlyticsErrorHandler : ErrorHandler { override fun reportException(t: Throwable, message: String?) { @Suppress("ConstantConditionIf") if (BuildConfig.DEBUG) { - Log.e("CrashlyticsErrorHandler", "Caught exception: $message", t) + Timber.e(t, "Caught exception: $message") } crashlytics.recordException(t) } diff --git a/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt b/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt index 3806c0a..7dbd706 100644 --- a/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt +++ b/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt @@ -5,12 +5,12 @@ import android.app.Application import android.content.SharedPreferences import android.os.Bundle import android.os.SystemClock -import android.util.Log import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.android.play.core.review.ReviewManager import com.google.android.play.core.review.ReviewManagerFactory import com.wbrawner.simplemarkdown.view.activity.MainActivity +import timber.log.Timber private const val KEY_TIME_IN_APP = "timeInApp" @@ -27,6 +27,7 @@ object ReviewHelper : Application.ActivityLifecycleCallbacks { private lateinit var application: Application private lateinit var reviewManager: ReviewManager private lateinit var sharedPreferences: SharedPreferences + private lateinit var timber: Timber.Tree private var activityCount = 0 set(value) { field = if (value < 0) { @@ -40,14 +41,16 @@ object ReviewHelper : Application.ActivityLifecycleCallbacks { fun init( application: Application, sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application), - reviewManager: ReviewManager = ReviewManagerFactory.create(application) + reviewManager: ReviewManager = ReviewManagerFactory.create(application), + timber: Timber.Tree = Timber.asTree() ) { this.application = application this.sharedPreferences = sharedPreferences this.reviewManager = reviewManager + this.timber = timber if (sharedPreferences.getLong(KEY_TIME_IN_APP, 0L) == -1L) { // We've already prompted the user for the review so let's not be annoying about it - Log.i("ReviewHelper", "User already prompted for review, not configuring ReviewHelper") + timber.i("User already prompted for review, not configuring ReviewHelper") return } application.registerActivityLifecycleCallbacks(this) @@ -63,24 +66,24 @@ object ReviewHelper : Application.ActivityLifecycleCallbacks { } if (activity !is MainActivity || sharedPreferences.getLong(KEY_TIME_IN_APP, 0L) < TIME_TO_PROMPT) { // Not ready to prompt just yet - Log.v("ReviewHelper", "Not ready to prompt user for review yet") + timber.v("Not ready to prompt user for review yet") return } - Log.v("ReviewHelper", "Prompting user for review") + timber.v("Prompting user for review") reviewManager.requestReviewFlow().addOnCompleteListener { request -> if (!request.isSuccessful) { val exception = request.exception ?: RuntimeException("Failed to request review") - Log.e("ReviewHelper", "Failed to prompt user for review", exception) + timber.e(exception, "Failed to prompt user for review") return@addOnCompleteListener } - reviewManager.launchReviewFlow(activity, request.result).addOnCompleteListener { _ -> + reviewManager.launchReviewFlow(activity, request.result).addOnCompleteListener { // According to the docs, this may or may not have actually been shown. Either // way, it's not a critical piece of functionality for the app so I'm not // worried about it failing silently. Link for reference: // https://developer.android.com/guide/playcore/in-app-review/kotlin-java#launch-review-flow - Log.v("ReviewHelper", "User finished review, ending activity watch") + timber.v("User finished review, ending activity watch") application.unregisterActivityLifecycleCallbacks(this) sharedPreferences.edit { putLong(KEY_TIME_IN_APP, -1L)