diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt index 12b7ba8d..0ad492ed 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt @@ -51,6 +51,7 @@ class App1 : Application() { override fun onCreate() { log.d("onCreate") super.onCreate() + initializeToastUtils(this) prepare(applicationContext, "App1.onCreate") } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnActions.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnActions.kt index f77612f4..1a500fdf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnActions.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnActions.kt @@ -452,14 +452,15 @@ fun Column.onTagFollowChanged(account: SavedAccount, newTag: TootTag) { } } if (type == ColumnType.FOLLOWED_HASHTAGS) { - val tagFinder:(TimelineItem)->Boolean = {it is TootTag && it.name == newTag.name} + val tagFinder: (TimelineItem) -> Boolean = + { it is TootTag && it.name == newTag.name } when (newTag.following) { true -> if (tmpList.none(tagFinder)) { tmpList.add(0, newTag) } else -> tmpList.indexOfFirst(tagFinder) - .takeIf { it>=0 }?.let{ tmpList.removeAt(it)} + .takeIf { it >= 0 }?.let { tmpList.removeAt(it) } } } listData.clear() diff --git a/app/src/main/java/jp/juggler/util/ToastUtils.kt b/app/src/main/java/jp/juggler/util/ToastUtils.kt index 00ab249c..cc35d163 100644 --- a/app/src/main/java/jp/juggler/util/ToastUtils.kt +++ b/app/src/main/java/jp/juggler/util/ToastUtils.kt @@ -1,21 +1,177 @@ package jp.juggler.util +import android.app.Activity +import android.app.Application import android.content.Context +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 import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import jp.juggler.subwaytooter.R -import kotlinx.coroutines.CancellationException +import jp.juggler.subwaytooter.databinding.PopupToastBinding +import kotlinx.coroutines.* import me.drakeet.support.toast.ToastCompat import java.lang.ref.WeakReference +import kotlin.coroutines.resume private val log = LogCategory("ToastUtils") private var refToast: WeakReference? = null +private var oldApplication: WeakReference? = null +private var lastActivity: WeakReference? = null +private var lastPopup: WeakReference? = 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) { + 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.") + } + +private fun showPopup(activity: Activity, bLong: Boolean, message: String) { + val rootView = activity.findViewById(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.") + } + lastPopup = WeakReference(popupWindow) + + popupWindow.showAtLocation(rootView, Gravity.CENTER, 0, 0) + + 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.") + } + } +} internal fun showToastImpl(context: Context, bLong: Boolean, message: String): Boolean { runOnMainLooper { + // Android 12以降はトーストを全文表示しないので、何か画面が表示中ならポップアップウィンドウを使う + lastActivity?.get()?.let { + try { + showPopup(it, bLong, message) + return@runOnMainLooper + } catch (ex: Throwable) { + log.trace(ex, "showPopup failed.") + } + } + // 画面がない、または失敗したら普通のトーストにフォールバック + // 前回のトーストの表示を終了する try { refToast?.get()?.cancel() diff --git a/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt b/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt index a58925c6..93efe95b 100644 --- a/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt +++ b/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt @@ -22,6 +22,7 @@ import android.view.WindowManager import android.view.WindowManager.BadTokenException import android.widget.Toast import androidx.annotation.StringRes +import jp.juggler.util.LogCategory fun interface BadTokenListener { @@ -58,6 +59,7 @@ internal class SafeToastContext(base: Context, private val toast: Toast) : Conte private inner class WindowManagerWrapper(private val base: WindowManager) : WindowManager { @Suppress("DEPRECATION") + @Deprecated("Use Context.getDisplay() instead.") override fun getDefaultDisplay(): Display? = base.defaultDisplay @@ -87,10 +89,11 @@ internal class SafeToastContext(base: Context, private val toast: Toast) : Conte @Suppress("TooManyFunctions") class ToastCompat private constructor( context: Context, - private val base: Toast + private val base: Toast, ) : Toast(context) { companion object { + private val log = LogCategory("ToastCompat") @SuppressLint("DiscouragedPrivateApi") private fun setContextCompat(view: View?, contextCreator: () -> Context) { @@ -99,9 +102,8 @@ class ToastCompat private constructor( val field = View::class.java.getDeclaredField("mContext") field.isAccessible = true field[view] = contextCreator() - } catch (throwable: Throwable) { - @Suppress("PrintStackTrace") - throwable.printStackTrace() + } catch (ex: Throwable) { + log.trace(ex) } } } @@ -129,8 +131,7 @@ class ToastCompat private constructor( * @param duration How long to display the message. Either [.LENGTH_SHORT] or * [.LENGTH_LONG] */ - @SuppressLint("ShowToast") - @Suppress("DEPRECATION") + @Suppress("ShowToast", "DEPRECATION") fun makeText(context: Context, text: CharSequence?, duration: Int): ToastCompat { // We cannot pass the SafeToastContext to Toast.makeText() because // the View will unwrap the base context and we are in vain. @@ -148,12 +149,14 @@ class ToastCompat private constructor( } @Suppress("DEPRECATION") + @Deprecated(message = "Custom toast views are deprecated in API level 30.") override fun setView(view: View) { base.view = view setContextCompat(base.view) { SafeToastContext(view.context, base) } } @Suppress("DEPRECATION") + @Deprecated(message = "Custom toast views are deprecated in API level 30.") override fun getView(): View? = base.view override fun show() = base.show() diff --git a/app/src/main/res/drawable-hdpi/ic_app_popup.png b/app/src/main/res/drawable-hdpi/ic_app_popup.png new file mode 100644 index 00000000..c8dc2362 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_app_popup.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_app_popup.png b/app/src/main/res/drawable-mdpi/ic_app_popup.png new file mode 100644 index 00000000..8b664bc0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_app_popup.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_app_popup.png b/app/src/main/res/drawable-xhdpi/ic_app_popup.png new file mode 100644 index 00000000..f4d4cb12 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_app_popup.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_app_popup.png b/app/src/main/res/drawable-xxhdpi/ic_app_popup.png new file mode 100644 index 00000000..63a7c984 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_app_popup.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_app_popup.png b/app/src/main/res/drawable-xxxhdpi/ic_app_popup.png new file mode 100644 index 00000000..e9adff54 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_app_popup.png differ diff --git a/app/src/main/res/layout/popup_toast.xml b/app/src/main/res/layout/popup_toast.xml new file mode 100644 index 00000000..6bcda4af --- /dev/null +++ b/app/src/main/res/layout/popup_toast.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5a73fdc7..d3164fe3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -34,7 +34,7 @@ #cccccc - #8333 + #D222 #eee @@ -62,7 +62,7 @@ #66FFFFFF #BBBBBB #dddddd - #8333 + #D222 #e4e4e4 #e4e4e4