Toast-PhoneGap-Plugin icon indicating copy to clipboard operation
Toast-PhoneGap-Plugin copied to clipboard

WIP: Fix for API 30 / Android 11 by removing deprecated API calls.

Open dpa99c opened this issue 3 years ago • 12 comments

Resolves #136.

Docs need updating as textColor is now the only styling option supported by Android 11. Also touch event is never sent as it can't be calculated due to removal of deprecated Toast.getView().

dpa99c avatar Jun 28 '21 12:06 dpa99c

@shahdeep1989 Toast.Callback was only added in API 30 so currently it won't build with lower APIs (i.e. cordova-android@9)

This PR fixes the plugin so it builds with API 30 / Android 11 for the upcoming Google deadline and therefore requires cordova-android@10 which is currently under development so you'll need to add cordova-android@nightly for now until it's released.

If I have time, I'll update this PR with conditionality so it continues to work when building against older API verions.

dpa99c avatar Jul 08 '21 07:07 dpa99c

Thanks, this fixed it for me after upgrading to cordova-android@10

I had the crash when clicking anywhere on the page even after the toast disappeared

QuentinFarizon avatar Aug 30 '21 16:08 QuentinFarizon

is this going in @EddyVerbruggen ?

souly1 avatar Sep 06 '21 07:09 souly1

Guys, cordova-android@10 is out. Any plans to merge this and release a new version?

andreyluiz avatar Sep 08 '21 18:09 andreyluiz

@dpa99c In fact, I don't know why it appeared to work the other day, but I know encounter this issue at runtime : I am on cordova-android@10 and checked that I'm targeting API 30

java.lang.NoClassDefFoundError: Failed resolution of: Landroid/widget/Toast$Callback;
        at nl.xservices.plugins.Toast.execute(Toast.java:79)
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98)
        at org.apache.cordova.PluginManager.exec(PluginManager.java:140)
        at org.apache.cordova.CordovaBridge.jsExec(CordovaBridge.java:59)
        at org.apache.cordova.engine.SystemExposedJsApi.exec(SystemExposedJsApi.java:41)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:336)
        at android.os.Looper.loop(Looper.java:174)
        at android.os.HandlerThread.run(HandlerThread.java:67)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "android.widget.Toast$Callback" on path: DexPathList[[zip file "/data/app/<my-package>-HwzFgL4UNfy44xNSnfn_VQ==/base.apk"],nativeLibraryDirectories=[/data/app/<my-package>-HwzFgL4UNfy44xNSnfn_VQ==/lib/x86, /data/app/<my-package>-HwzFgL4UNfy44xNSnfn_VQ==/base.apk!/lib/x86, /system/lib, /system/product/lib]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at nl.xservices.plugins.Toast.execute(Toast.java:79) 
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98) 
        at org.apache.cordova.PluginManager.exec(PluginManager.java:140) 
        at org.apache.cordova.CordovaBridge.jsExec(CordovaBridge.java:59) 
        at org.apache.cordova.engine.SystemExposedJsApi.exec(SystemExposedJsApi.java:41) 
        at android.os.MessageQueue.nativePollOnce(Native Method) 
        at android.os.MessageQueue.next(MessageQueue.java:336) 
        at android.os.Looper.loop(Looper.java:174) 
        at android.os.HandlerThread.run(HandlerThread.java:67) 

QuentinFarizon avatar Sep 11 '21 18:09 QuentinFarizon

Happy to merge this, but it's not entirely clear to me what the state is. Is it backward compatible and are the docs fine? Perhaps anyone want to test this PR? Cheers!

EddyVerbruggen avatar Sep 13 '21 16:09 EddyVerbruggen

TBH I haven't finished this PR off - just hacked out the deprecated API calls to make it compile under Android 11/API 30. it could do with conditionally supporting other options for older Android versions and the docs updating. Though toasts on Android 11 are so limited you may just want to replace this plugin with a web layer emulation of toast - I did

dpa99c avatar Sep 13 '21 16:09 dpa99c

UPDATE - ok, sadly it seems this is still crashing:

Fatal Exception: java.lang.NoClassDefFoundError
Failed resolution of: Landroid/widget/Toast$Callback

nl.xservices.plugins.Toast.execute (Toast.java:79)

souly1 avatar Sep 15 '21 06:09 souly1

Maybe we can go with https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin/pull/140 first before this PR is ready?

zommerfelds avatar Sep 20 '21 18:09 zommerfelds

This PR definitely fixes the bug for Android 11, BUT the downside is, that you now will have crashes on all the older Android versions as @souly1 and @QuentinFarizon already stated before.

The reason for the crashes is the use of Toast.Callback which was just added with API Level 30 -> https://developer.android.google.cn/reference/android/widget/Toast.Callback

I fixed that problem by re-adding lines 107-193 and by adding some additional Android version checks. It still needs to be tested on older Android versions, but I didn't have any crashes yet.

package nl.xservices.plugins;

import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.CountDownTimer;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AlignmentSpan;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class Toast extends CordovaPlugin {

  private static final String ACTION_SHOW_EVENT = "show";
  private static final String ACTION_HIDE_EVENT = "hide";

  private static final int GRAVITY_TOP = Gravity.TOP|Gravity.CENTER_HORIZONTAL;
  private static final int GRAVITY_CENTER = Gravity.CENTER_VERTICAL|Gravity.CENTER_HORIZONTAL;
  private static final int GRAVITY_BOTTOM = Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL;

  private static final int BASE_TOP_BOTTOM_OFFSET = 20;

  private android.widget.Toast mostRecentToast;
  private ViewGroup viewGroup;

  private static final boolean IS_AT_LEAST_LOLLIPOP = Build.VERSION.SDK_INT >= 21;
  private static final boolean IS_AT_LEAST_ANDROID_11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;

  // note that webView.isPaused() is not Xwalk compatible, so tracking it poor-man style
  private boolean isPaused;

  private String currentMessage;
  private JSONObject currentData;
  private static CountDownTimer _timer;

  @Override
  public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if (ACTION_HIDE_EVENT.equals(action)) {
      returnTapEvent("hide", currentMessage, currentData, callbackContext);
      hide();
      callbackContext.success();
      return true;

    } else if (ACTION_SHOW_EVENT.equals(action)) {
      if (this.isPaused) {
        return true;
      }

      final JSONObject options = args.getJSONObject(0);
      final String duration = options.getString("duration");
      final String position = options.getString("position");
      final int addPixelsY = options.has("addPixelsY") ? options.getInt("addPixelsY") : 0;
      final JSONObject data = options.has("data") ? options.getJSONObject("data") : null;
      JSONObject styling = options.optJSONObject("styling");
      final String msg = options.getString("message");
      currentMessage = msg;
      currentData = data;

      String _msg = msg;
      if(styling != null){
        final String textColor = styling.optString("textColor", "#000000");
        _msg = "<font color='"+textColor+"' ><b>" + _msg + "</b></font>";
      }
      final String html = _msg;

      cordova.getActivity().runOnUiThread(new Runnable() {
        public void run() {
          int hideAfterMs;
          if ("short".equalsIgnoreCase(duration)) {
            hideAfterMs = 2000;
          } else if ("long".equalsIgnoreCase(duration)) {
            hideAfterMs = 4000;
          } else {
            // assuming a number of ms
            hideAfterMs = Integer.parseInt(duration);
          }

          Spanned message;
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // FROM_HTML_MODE_LEGACY is the behaviour that was used for versions below android N
            // we are using this flag to give a consistent behaviour
            message = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
          } else {
            message = Html.fromHtml(html);
          }

          final android.widget.Toast toast = android.widget.Toast.makeText(
            IS_AT_LEAST_LOLLIPOP ? cordova.getActivity().getWindow().getContext() : cordova.getActivity().getApplicationContext(),
            message,
            "short".equalsIgnoreCase(duration) ? android.widget.Toast.LENGTH_SHORT : android.widget.Toast.LENGTH_LONG
          );

          if ("top".equals(position)) {
            toast.setGravity(GRAVITY_TOP, 0, BASE_TOP_BOTTOM_OFFSET + addPixelsY);
          } else  if ("bottom".equals(position)) {
            toast.setGravity(GRAVITY_BOTTOM, 0, BASE_TOP_BOTTOM_OFFSET - addPixelsY);
          } else if ("center".equals(position)) {
            toast.setGravity(GRAVITY_CENTER, 0, addPixelsY);
          } else {
            callbackContext.error("invalid position. valid options are 'top', 'center' and 'bottom'");
            return;
          }

          // On Android >= 5 you can no longer rely on the 'toast.getView().setOnTouchListener',
          // so created something funky that compares the Toast position to the tap coordinates.
          if (IS_AT_LEAST_LOLLIPOP) {
            if (IS_AT_LEAST_ANDROID_11) {
              toast.addCallback(new android.widget.Toast.Callback(){
                public void onToastShown() {}
                public void onToastHidden() {
                  returnTapEvent("hide", msg, data, callbackContext);
                }
              });
            } else {
              getViewGroup().setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                  if (motionEvent.getAction() != MotionEvent.ACTION_DOWN) {
                    return false;
                  }
                  if (mostRecentToast == null || !mostRecentToast.getView().isShown()) {
                    getViewGroup().setOnTouchListener(null);
                    return false;
                  }

                  float w = mostRecentToast.getView().getWidth();
                  float startX = (view.getWidth() / 2) - (w / 2);
                  float endX = (view.getWidth() / 2) + (w / 2);

                  float startY;
                  float endY;

                  float g = mostRecentToast.getGravity();
                  float y = mostRecentToast.getYOffset();
                  float h = mostRecentToast.getView().getHeight();

                  if (g == GRAVITY_BOTTOM) {
                    startY = view.getHeight() - y - h;
                    endY = view.getHeight() - y;
                  } else if (g == GRAVITY_CENTER) {
                    startY = (view.getHeight() / 2) + y - (h / 2);
                    endY = (view.getHeight() / 2) + y + (h / 2);
                  } else {
                    // top
                    startY = y;
                    endY = y + h;
                  }

                  float tapX = motionEvent.getX();
                  float tapY = motionEvent.getY();

                  final boolean tapped = tapX >= startX && tapX <= endX &&
                    tapY >= startY && tapY <= endY;

                  return tapped && returnTapEvent("touch", msg, data, callbackContext);
                }
              });
            }

          } else {
            toast.getView().setOnTouchListener(new View.OnTouchListener() {
              @Override
              public boolean onTouch(View view, MotionEvent motionEvent) {
                return motionEvent.getAction() == MotionEvent.ACTION_DOWN && returnTapEvent("touch", msg, data, callbackContext);
              }
            });
          }
          // trigger show every 2500 ms for as long as the requested duration
          _timer = new CountDownTimer(hideAfterMs, 2500) {
            public void onTick(long millisUntilFinished) {
              // see https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin/issues/116
              // and https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin/issues/120
//              if (!IS_AT_LEAST_PIE) {
//                toast.show();
//              }
            }
            public void onFinish() {
              returnTapEvent("hide", msg, data, callbackContext);
              toast.cancel();
            }
          }.start();

          mostRecentToast = toast;
          toast.show();

          PluginResult pr = new PluginResult(PluginResult.Status.OK);
          pr.setKeepCallback(true);
          callbackContext.sendPluginResult(pr);
        }
      });

      return true;
    } else {
      callbackContext.error("toast." + action + " is not a supported function. Did you mean '" + ACTION_SHOW_EVENT + "'?");
      return false;
    }
  }


  private void hide() {
    if (mostRecentToast != null) {
      mostRecentToast.cancel();
      getViewGroup().setOnTouchListener(null);
    }
    if (_timer != null) {
      _timer.cancel();
    }
  }

  private boolean returnTapEvent(String eventName, String message, JSONObject data, CallbackContext callbackContext) {
    final JSONObject json = new JSONObject();
    try {
      json.put("event", eventName);
      json.put("message", message);
      json.put("data", data);
    } catch (JSONException e) {
      e.printStackTrace();
    }
    callbackContext.success(json);
    return true;
  }

  // lazy init and caching
  private ViewGroup getViewGroup() {
    if (viewGroup == null) {
      viewGroup = (ViewGroup) ((ViewGroup) cordova.getActivity().findViewById(android.R.id.content)).getChildAt(0);
    }
    return viewGroup;
  }

  @Override
  public void onPause(boolean multitasking) {
    hide();
    this.isPaused = true;
  }

  @Override
  public void onResume(boolean multitasking) {
    this.isPaused = false;
  }
}

vorderpneu avatar Sep 27 '21 08:09 vorderpneu

I tried out the code above from @vorderpneu and it seems to work on both API 30 and lower (tested on android 11 emulator and android 5.1.1 device).

rastafan avatar Oct 12 '21 15:10 rastafan

@rastafan cool, but be careful and test this fix on different devices. In my case it looked crappy on Samsung devices. That's why I replaced this plugin with the ion-toast component in the end

vorderpneu avatar Oct 12 '21 15:10 vorderpneu