2018-12-01 00:02:18 +01:00
|
|
|
package jp.juggler.util
|
|
|
|
|
2022-07-23 14:55:07 +02:00
|
|
|
import android.app.Activity
|
|
|
|
import android.app.Application
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.content.Context
|
2022-07-23 14:55:07 +02:00
|
|
|
import android.os.Bundle
|
|
|
|
import android.util.Log
|
|
|
|
import android.view.Gravity
|
|
|
|
import android.view.View
|
|
|
|
import android.view.WindowManager
|
|
|
|
import android.view.animation.AlphaAnimation
|
|
|
|
import android.view.animation.Animation
|
|
|
|
import android.widget.PopupWindow
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.widget.Toast
|
2022-05-30 14:32:12 +02:00
|
|
|
import androidx.annotation.StringRes
|
2022-05-29 15:38:21 +02:00
|
|
|
import androidx.appcompat.app.AlertDialog
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
|
import jp.juggler.subwaytooter.R
|
2022-07-23 14:55:07 +02:00
|
|
|
import jp.juggler.subwaytooter.databinding.PopupToastBinding
|
|
|
|
import kotlinx.coroutines.*
|
2018-12-01 00:02:18 +01:00
|
|
|
import me.drakeet.support.toast.ToastCompat
|
2020-09-09 21:46:50 +02:00
|
|
|
import java.lang.ref.WeakReference
|
2022-07-23 14:55:07 +02:00
|
|
|
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
|
2022-07-23 14:55:07 +02:00
|
|
|
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 {
|
|
|
|
withTimeout(duration + 333L) {
|
2022-08-08 03:42:01 +02:00
|
|
|
suspendCancellableCoroutine { cont ->
|
2022-07-23 14:55:07 +02:00
|
|
|
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.")
|
2022-08-08 03:42:01 +02:00
|
|
|
Unit
|
2022-07-23 14:55:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun showPopup(activity: Activity, bLong: Boolean, message: String) {
|
|
|
|
val rootView = activity.findViewById<View?>(android.R.id.content)?.rootView
|
|
|
|
?: error("missing rootView")
|
|
|
|
|
|
|
|
val views = PopupToastBinding.inflate(activity.layoutInflater)
|
|
|
|
views.tvMessage.text = message
|
|
|
|
|
|
|
|
val popupWindow = PopupWindow(
|
|
|
|
views.root,
|
|
|
|
WindowManager.LayoutParams.MATCH_PARENT,
|
|
|
|
WindowManager.LayoutParams.MATCH_PARENT,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
|
|
|
|
// タップ時に他のViewでキャッチされないための設定
|
|
|
|
popupWindow.isFocusable = false
|
|
|
|
popupWindow.isTouchable = false
|
|
|
|
popupWindow.isOutsideTouchable = false
|
|
|
|
|
|
|
|
try {
|
|
|
|
lastPopup?.get()?.dismiss()
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.trace(ex, "dismiss failed.")
|
|
|
|
}
|
|
|
|
|
2022-08-08 03:42:01 +02:00
|
|
|
lastPopup = null
|
2022-07-23 14:55:07 +02:00
|
|
|
popupWindow.showAtLocation(rootView, Gravity.CENTER, 0, 0)
|
2022-08-08 03:42:01 +02:00
|
|
|
lastPopup = WeakReference(popupWindow)
|
2022-07-23 14:55:07 +02:00
|
|
|
|
|
|
|
launchMain {
|
|
|
|
|
|
|
|
// fade in
|
|
|
|
AlphaAnimation(0.1f, 1f)
|
|
|
|
.startAndAwait(333L, views.tvMessage)
|
|
|
|
|
|
|
|
// keep
|
|
|
|
val keepDuration = when {
|
|
|
|
bLong -> 4000L
|
|
|
|
else -> 2000L
|
|
|
|
}
|
|
|
|
delay(keepDuration)
|
|
|
|
|
|
|
|
// fade out
|
|
|
|
AlphaAnimation(1f, 0f)
|
|
|
|
.startAndAwait(333L, views.tvMessage)
|
|
|
|
|
|
|
|
// dismiss
|
|
|
|
try {
|
|
|
|
popupWindow.dismiss()
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.e(ex, "dismiss failed.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
|
|
|
|
internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean {
|
|
|
|
runOnMainLooper {
|
|
|
|
|
2022-08-16 17:39:52 +02:00
|
|
|
if (message.count { it == '\n' } > 1) {
|
|
|
|
// Android 12以降はトーストを全文表示しないので、何か画面が表示中ならポップアップウィンドウを使う
|
|
|
|
lastActivity?.get()?.let {
|
|
|
|
try {
|
|
|
|
showPopup(it, bLong, message)
|
|
|
|
return@runOnMainLooper
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.trace(ex, "showPopup failed.")
|
|
|
|
}
|
2022-07-23 14:55:07 +02:00
|
|
|
}
|
2022-08-16 17:39:52 +02:00
|
|
|
// 画面がない、または失敗したら普通のトーストにフォールバック
|
2022-07-23 14:55:07 +02:00
|
|
|
}
|
|
|
|
|
2022-05-29 15:38:21 +02:00
|
|
|
// 前回のトーストの表示を終了する
|
|
|
|
try {
|
|
|
|
refToast?.get()?.cancel()
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.trace(ex)
|
|
|
|
} finally {
|
|
|
|
refToast = null
|
2021-06-20 15:12:25 +02:00
|
|
|
}
|
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.trace(ex)
|
|
|
|
}
|
|
|
|
|
|
|
|
// コールスタックの外側でエラーになる…
|
|
|
|
// android.view.WindowManager$BadTokenException:
|
|
|
|
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
|
|
|
|
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
|
|
|
|
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
|
|
|
|
// at android.widget.Toast$TN.handleShow (Toast.java:435)
|
|
|
|
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
|
2021-06-20 15:12:25 +02:00
|
|
|
}
|
2022-05-29 15:38:21 +02:00
|
|
|
return false
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
fun Context.showToast(bLong: Boolean, caption: String?): Boolean =
|
2022-05-29 15:38:21 +02:00
|
|
|
showToastImpl(this, bLong, caption ?: "(null)")
|
2018-12-01 00:02:18 +01:00
|
|
|
|
2022-05-30 14:32:12 +02: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
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
fun Context.showToast(bLong: Boolean, 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
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
fun Context.showToast(ex: Throwable, stringId: Int, vararg args: Any): Boolean =
|
2022-05-29 15:38:21 +02:00
|
|
|
showToastImpl(this, true, ex.withCaption(resources, stringId, *args))
|
|
|
|
|
2022-05-30 14:32:12 +02:00
|
|
|
fun AppCompatActivity.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 {
|
|
|
|
AlertDialog.Builder(this)
|
|
|
|
.setTitle(R.string.error)
|
|
|
|
.setMessage(
|
|
|
|
listOf(
|
|
|
|
caption,
|
|
|
|
when (ex) {
|
2022-05-30 14:32:12 +02:00
|
|
|
is IllegalStateException -> null
|
|
|
|
else -> ex.javaClass.simpleName
|
2022-05-29 15:38:21 +02:00
|
|
|
},
|
|
|
|
ex.message,
|
|
|
|
)
|
|
|
|
.filter { !it.isNullOrBlank() }
|
|
|
|
.joinToString("\n")
|
|
|
|
)
|
|
|
|
.setPositiveButton(R.string.ok, null)
|
2022-05-30 14:32:12 +02:00
|
|
|
.show()
|
|
|
|
} catch (ignored: Throwable) {
|
2022-05-29 15:38:21 +02:00
|
|
|
showToast(ex, caption)
|
|
|
|
}
|
|
|
|
}
|
2022-05-30 14:32:12 +02:00
|
|
|
|
|
|
|
fun Context.errorString(@StringRes stringId: Int, vararg args: Any?): Nothing =
|
2022-06-03 16:23:44 +02:00
|
|
|
error(getString(stringId, *args))
|