Use Timber for logging
This commit is contained in:
parent
96e7b7c6b3
commit
14dc55433a
11 changed files with 224 additions and 54 deletions
|
@ -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'
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<SimpleDateFormat>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String?>("Untitled.md")
|
||||
val markdownUpdates = MutableLiveData<String>()
|
||||
val editorActions = MutableLiveData<EditorAction>()
|
||||
|
@ -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("")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue