Checking Activity.isFinishing
was not sufficient like others mentioned.
Zebra Android devices that are stuck at 7.1 kept crashing.
Thankfully, the ToastCompat
library did prevent the crashes.
However, we were working with Xamarin.Android
.
We ported the PureWriter/ToastCompat
library to Xamarin/C#, below is how to use.
Example of use:
using Droid.ToastCompat;// Copy Droid.ToastCompat from the code example below
ToastCompat.MakeText(context: this, message, duration, onBadTokenCaught: toast =>
{
// A bad token often means the underlining race condition
// in Android 7.1 was hit but the activity is typically still running
// We will resend the toast to give it another shot
ToastCompat.MakeText(context: this, message, duration, onBadTokenCaught: toast =>
{
// Ok we did all we could, user will not be getting a toast but will also not be crashing
})
.Show();
})
.Show();
PureWriter/ToastCompat in Xamarin in a single file:
using System;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
namespace Droid
{
/// <summary>
/// This is a Xamarin.Android port of https://github.com/PureWriter/ToastCompat
///
/// It addresses a race condition in Android 7.1 which can sometimes result
/// in a <see cref="WindowManagerBadTokenException"/> being thrown when
/// displaying a toast. Because the exception is in async Android OS code we
/// can not directly wrap with a try/catch. We need to wrap the context the
/// toast uses and supply a wrapped <see cref="IWindowManager"/> which allows
/// us a then try/catch where the exception is thrown.
///
/// Copyright 2017 drakeet.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
/// </summary>
public class ToastCompat : Toast
{
/// <summary>
/// Called when a debug statement can be logged. By default it uses the
/// default Android logger, but can be set to invoke a custom logger.
/// </summary>
public static Action<(string tag, string statement)> LogDebug { get; set; } = (info) => { Android.Util.Log.Debug(info.tag, info.statement); };
/// <summary>
/// Called when an exception can be logged. By default it uses the
/// default Android logger, but can be set to invoke a custom logger.
/// </summary>
public static Action<(string tag, string statement, Exception exception)> LogException { get; set; } = (info) => { Android.Util.Log.Error(info.tag, info.statement, info.exception); };
private readonly static string TAG = nameof(ToastCompat);
private readonly Toast _toast;
/// <summary>
/// The toast message, useful for tracking when exception occurs based
/// on what toast message is failing to display.
/// </summary>
private readonly string _toastMessage;
/// <summary>
/// Called when a <see cref="WindowManagerBadTokenException"/> which only happens in Android 7.1
/// </summary>
private readonly Action<Toast> _onBadTokenCaught;
/// <summary>
/// Construct an empty Toast object.
/// You must call <see cref="SetContextCompat(View, Context)"/>
/// before you can call <see cref="Show"/>.
/// </summary>
/// <param name="context">The context to use. Usually Application or Activity.</param>
/// <param name="toast">The base toast</param>
/// <param name="_onBadTokenCaught">
/// The callback when a <see cref="WindowManagerBadTokenException"/> is thrown
/// when trying to display a toast.
/// </param>
private ToastCompat(Context context, Toast toast, string toastMessage, Action<Toast> onBadTokenCaught) : base(context)
{
_toast = toast;
_toastMessage = toastMessage;
_onBadTokenCaught = onBadTokenCaught;
}
/// <summary>
/// Make a standard toast that just contains a text view.
/// </summary>
/// <param name="context">The context to use. Usually Application or Activity.</param>
/// <param name="text">The text to display</param>
/// <param name="duration">How long to display the message. </param>
/// <param name="onBadTokenCaught">
/// The callback when a toast fails to display with the toast as a argument for the callback.
///
/// Typically this would be a good place to try displaying the toast one
/// more time because the Activity may very well be still running and we
/// just ran into the Android OS race condition.
/// </param>
public static ToastCompat MakeText(Context context, string text, ToastLength duration, Action<Toast> onBadTokenCaught)
{
// We cannot pass the SafeToastContext to Toast.makeText() because
// the View will unwrap the base context and we are in vain.
var toast = Toast.MakeText(context, text, duration)!;
#pragma warning disable CS0618 // Type or member is obsolete
if (toast?.View is View view)
#pragma warning restore CS0618 // Type or member is obsolete
{
SetContextCompat(view, new SafeToastContext(context, text, onBadTokenCaught: () => onBadTokenCaught(toast)));
}
else
{
LogDebug((TAG, "Toast failed to apply ToastCompat fix because it's view is null"));
}
return new ToastCompat(context, toast!, text, onBadTokenCaught);
}
public override ToastLength Duration
{
get => _toast.Duration;
set => _toast.Duration = value;
}
public override void Show() => _toast.Show();
public override void SetGravity([GeneratedEnum] GravityFlags gravity, int xOffset, int yOffset) => _toast.SetGravity(gravity, xOffset, yOffset);
public override void SetMargin(float horizontalMargin, float verticalMargin) => _toast.SetMargin(horizontalMargin, verticalMargin);
public override void SetText(int resId) => _toast.SetText(resId);
public override void SetText(Java.Lang.ICharSequence? s) => _toast.SetText(s);
public new void SetText(string s) => _toast.SetText(s);
public override float HorizontalMargin => _toast.HorizontalMargin;
public override float VerticalMargin => _toast.VerticalMargin;
public override GravityFlags Gravity => _toast.Gravity;
public override int XOffset => _toast.XOffset;
public override int YOffset => _toast.YOffset;
[Obsolete]
public override View? View
{
get => _toast.View;
set
{
_toast.View = value;
if (value is not null)
{
if (value.Context is Context context)
{
SetContextCompat(value, new SafeToastContext(value.Context, _toastMessage, onBadTokenCaught: () => _onBadTokenCaught(_toast)));
}
else
{
LogDebug((TAG, "Toast failed to apply ToastCompat fix because the new view's context is null"));
}
}
}
}
private static void SetContextCompat(View view, Context context)
{
if (Android.OS.Build.VERSION.SdkInt == Android.OS.BuildVersionCodes.NMr1)
{
try
{
var field = Java.Lang.Class.FromType(typeof(View)).GetDeclaredField("mContext");
field.Accessible = true;
field.Set(view, context);
}
catch (Exception e)
{
LogException((TAG, $"Failed to {nameof(SetContextCompat)}", e));
}
}
}
private class SafeToastContext : ContextWrapper
{
private readonly string _toastMessage;
private readonly Action _onBadTokenCaught;
public SafeToastContext(Context context, string toastMessage, Action onBadTokenCaught) : base(context)
{
_toastMessage = toastMessage;
_onBadTokenCaught = onBadTokenCaught;
}
public override Context? ApplicationContext => new ApplicationContextWrapper(BaseContext!.ApplicationContext!, _toastMessage, _onBadTokenCaught);
private sealed class ApplicationContextWrapper : ContextWrapper
{
private readonly string _toastMessage;
private readonly Action _onBadTokenCaught;
public ApplicationContextWrapper(Context context, string toastMessage, Action onBadTokenCaught) : base(context)
{
_toastMessage = toastMessage;
_onBadTokenCaught = onBadTokenCaught;
}
public override Java.Lang.Object? GetSystemService(string? name)
{
var service = base.GetSystemService(name);
// Override the window manager so we can capture the bad token exception
if (Context.WindowService == name)
{
if (service.JavaCast<IWindowManager>() is IWindowManager windowManager)
{
return new WindowManagerWrapper(windowManager!, _toastMessage, _onBadTokenCaught);
}
LogDebug((TAG, $"Failed to cast window service to {nameof(IWindowManager)}. ToastCompat fix not applied."));
}
return service;
}
}
/// <summary>
/// Wraps <see cref="IWindowManager"/> in order to override <see cref="IViewManager.AddView(View?, ViewGroup.LayoutParams?)"/>
/// which allows us to catch and handle the thrown <see cref="WindowManagerBadTokenException"/>.
/// </summary>
public sealed class WindowManagerWrapper : Java.Lang.Object, IWindowManager
{
private readonly static string TAG = "WindowManagerWrapper";
private readonly IWindowManager _windowManager;
private readonly string _toastMessage;
private readonly Action _onBadTokenCaught;
public WindowManagerWrapper(IWindowManager windowManager, string toastMessage, Action onBadTokenCaught)
{
_windowManager = windowManager;
_toastMessage = toastMessage;
_onBadTokenCaught = onBadTokenCaught;
}
void IViewManager.AddView(View? view, ViewGroup.LayoutParams? @params)
{
try
{
LogDebug((TAG, "WindowManager's addView(view, params) has been hooked."));
// NOTE: Set a breakpoint here and you will always throw a WindowManagerBadTokenException exception since it will cause the underlining race condition to be true
_windowManager.AddView(view, @params);
}
catch (WindowManagerBadTokenException e)
{
LogException((TAG, $"{nameof(WindowManagerBadTokenException)} caught when `{nameof(IViewManager.AddView)}` was called within `{nameof(WindowManagerWrapper)}` for toast message `{_toastMessage}`", e));
_onBadTokenCaught();
}
catch (Exception e)
{
LogException((TAG, "Unexpected Exception", e));
}
}
Display? IWindowManager.DefaultDisplay => _windowManager.DefaultDisplay;
void IViewManager.RemoveView(View? view) => _windowManager.RemoveView(view);
void IWindowManager.RemoveViewImmediate(View? view) => _windowManager.RemoveViewImmediate(view);
void IViewManager.UpdateViewLayout(View? view, ViewGroup.LayoutParams? @params) => _windowManager.UpdateViewLayout(view, @params);
}
}
}
}