Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dpa99c
Copy link

@dpa99c dpa99c commented Jun 28, 2021

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
Copy link
Author

dpa99c commented Jul 8, 2021

@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.

@QuentinFarizon
Copy link

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

@souly1
Copy link

souly1 commented Sep 6, 2021

is this going in @EddyVerbruggen ?

@andreyluiz
Copy link

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

@QuentinFarizon
Copy link

QuentinFarizon commented Sep 11, 2021

@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) 

@EddyVerbruggen
Copy link
Owner

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!

@dpa99c
Copy link
Author

dpa99c commented Sep 13, 2021

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

@souly1
Copy link

souly1 commented Sep 15, 2021

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)

@zommerfelds
Copy link

Maybe we can go with #140 first before this PR is ready?

@vorderpneu
Copy link

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;
  }
}

@rastafan
Copy link

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 added a commit to rastafan/Toast-PhoneGap-Plugin that referenced this pull request Oct 12, 2021
Fix for Android 11 (taken from @vorderpneu at EddyVerbruggen#139)
@vorderpneu
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

null pointer exceptions in android 11
10 participants