SubwayTooter-Android-App/base/src/main/java/jp/juggler/util/log/ToastUtils.kt

214 lines
7.1 KiB
Kotlin
Raw Normal View History

package jp.juggler.util.log
2018-12-01 00:02:18 +01:00
import android.app.Activity
import android.app.Application
2018-12-01 00:02:18 +01:00
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.animation.Animation
import android.widget.PopupWindow
2018-12-01 00:02:18 +01:00
import android.widget.Toast
import androidx.annotation.StringRes
2022-05-29 15:38:21 +02:00
import androidx.appcompat.app.AlertDialog
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.runOnMainLooper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
2018-12-01 00:02:18 +01:00
import me.drakeet.support.toast.ToastCompat
import java.lang.ref.WeakReference
import kotlin.coroutines.resume
2018-12-01 00:02:18 +01:00
2022-05-29 15:38:21 +02:00
private val log = LogCategory("ToastUtils")
private var refToast: WeakReference<Toast>? = null
private var oldApplication: WeakReference<Application>? = null
private var lastActivity: WeakReference<Activity>? = null
private var lastPopup: WeakReference<PopupWindow>? = null
private val activityCallback = object : Application.ActivityLifecycleCallbacks {
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
lastActivity = WeakReference(activity)
}
override fun onActivityResumed(activity: Activity) {
lastActivity = WeakReference(activity)
}
override fun onActivityPaused(activity: Activity) {
if (lastActivity?.get() == activity) {
lastActivity = null
}
}
override fun onActivityStopped(activity: Activity) {
if (lastActivity?.get() == activity) {
lastActivity = null
}
}
override fun onActivityDestroyed(activity: Activity) {
if (lastActivity?.get() == activity) {
lastActivity = null
}
}
}
/**
* App1.onCreateから呼ばれる
*/
fun initializeToastUtils(app: Application) {
try {
oldApplication?.get()?.unregisterActivityLifecycleCallbacks(activityCallback)
} catch (ex: Throwable) {
Log.e("SubwayTooter", "unregisterActivityLifecycleCallbacks failed.", ex)
}
try {
app.registerActivityLifecycleCallbacks(activityCallback)
} catch (ex: Throwable) {
Log.e("SubwayTooter", "registerActivityLifecycleCallbacks failed.", ex)
}
oldApplication = WeakReference(app)
}
/**
* Animationを開始して終了を非同期待機する
*/
suspend fun Animation.startAndAwait(duration: Long, v: View) =
try {
withTimeoutSafe(duration + 333L) {
suspendCancellableCoroutine { cont ->
v.clearAnimation()
this@startAndAwait.duration = duration
this@startAndAwait.fillAfter = true
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {}
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
cont.resume(Unit)
}
})
v.startAnimation(this@startAndAwait)
}
}
} catch (ex: TimeoutCancellationException) {
log.w(ex, "startAndAwait timeout.")
Unit
}
internal fun showToastImpl(
context: Context,
bLong: Boolean,
message: String,
forceToast: Boolean = false,
): Boolean {
2022-05-29 15:38:21 +02:00
runOnMainLooper {
if (!forceToast && (message.length >= 32 || message.count { it == '\n' } > 1)) {
// Android 12以降はトーストを全文表示しない
// 長いメッセージの場合は可能ならダイアログを使う
lastActivity?.get()?.let {
try {
AlertDialog.Builder(it)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show()
return@runOnMainLooper
} catch (ex: Throwable) {
log.e(ex, "showPopup failed.")
}
}
// 画面がない、または失敗したら普通のトーストにフォールバック
}
2022-05-29 15:38:21 +02:00
// 前回のトーストの表示を終了する
try {
refToast?.get()?.cancel()
} catch (ex: Throwable) {
log.e(ex, "toast cancel failed.")
2022-05-29 15:38:21 +02:00
} finally {
refToast = null
}
2022-05-29 15:38:21 +02:00
// 新しいトーストを作る
try {
val duration = if (bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
val t = ToastCompat.makeText(context, message, duration)
t.setBadTokenListener { }
t.show()
refToast = WeakReference(t)
} catch (ex: Throwable) {
log.e(ex, "showToastImpl failed.")
2022-05-29 15:38:21 +02:00
}
}
2022-05-29 15:38:21 +02:00
return false
2018-12-01 00:02:18 +01:00
}
fun Context.showToast(
bLong: Boolean,
caption: String?,
forceToast: Boolean = false,
): Boolean = showToastImpl(this, bLong, caption ?: "(null)", forceToast = forceToast)
2018-12-01 00:02:18 +01:00
fun Context.showToast(ex: Throwable, caption: String? = null): Boolean =
2022-05-29 15:38:21 +02:00
showToastImpl(this, true, ex.withCaption(caption))
2018-12-01 00:02:18 +01:00
fun Context.showToast(bLong: Boolean, @StringRes stringId: Int, vararg args: Any): Boolean =
2022-05-29 15:38:21 +02:00
showToastImpl(this, bLong, getString(stringId, *args))
2018-12-01 00:02:18 +01:00
fun Context.showToast(ex: Throwable, @StringRes stringId: Int, vararg args: Any): Boolean =
2022-05-29 15:38:21 +02:00
showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
fun Activity.dialogOrToast(message: String?) {
if (message.isNullOrBlank()) return
try {
android.app.AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show()
} catch (_: Throwable) {
showToast(true, message)
}
}
fun Activity.dialogOrToast(@StringRes stringId: Int, vararg args: Any) =
dialogOrToast(getString(stringId, *args))
fun Activity.showError(ex: Throwable, caption: String? = null) {
log.e(ex, caption ?: "(showError)")
2022-05-29 15:38:21 +02:00
// キャンセル例外はUIに表示しない
if (ex is CancellationException) return
try {
val text = listOf(
caption,
when (ex) {
is IllegalStateException -> null
else -> ex.javaClass.simpleName
},
ex.message,
)
.filter { !it.isNullOrBlank() }
.joinToString("\n")
if (text.isNotEmpty()) {
AlertDialog.Builder(this)
.setMessage(text)
.setPositiveButton(android.R.string.ok, null)
.show()
return
}
} catch (ignored: Throwable) {
2022-05-29 15:38:21 +02:00
}
showToast(ex, caption)
2022-05-29 15:38:21 +02:00
}
fun Context.errorString(@StringRes stringId: Int, vararg args: Any?): Nothing =
error(getString(stringId, *args))