Uncaught TypeError при использовании JavascriptInterface
В настоящее время я показываю кучу данных пользователю как HTML в веб-просмотре. У меня есть некоторые ссылки ниже каждой записи, которая должна вызывать метод в моем приложении при нажатии. Интерфейс JavaScript Android WebView кажется лучшим (только?) Способом обработки этих вещей. Однако, всякий раз, когда я нажимаю ссылку, я получаю это сообщение об ошибке: ERROR/Web Console(6112): Uncaught TypeError: Object [my namespace]@4075ff10 has no method 'edit' at [base URL]:55
Я объявляю следующий интерфейс:
public class JavaScriptInterface {
Context context;
JavaScriptInterface(Context c) {
context = c;
}
public void edit(String postid) {
Log.d("myApp", "EDIT!");
//do stuff
}
}
Затем добавлю его в свой WebView:
final WebView threadView = (WebView) findViewById(R.id.webViewThread);
threadView.getSettings().setJavaScriptEnabled(true);
threadView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
И, наконец, я называю это в своем HTML следующим образом:
<div class="post-actions">
<div class="right">
<a onClick="Android.edit('4312244');">Edit</a>
</div>
</div>
Настоящий кикер работает, когда я отлаживаю свое приложение через эмулятор или подключение adb к моему телефону. Когда я создаю и публикую приложение, оно ломается.
Я с ума сошел. Любая помощь или совет будут очень благодарны!
Ответы
Ответ 1
Такая же проблема для моего мобильного телефона 2.3.3.
Но поскольку я знал, что одно приложение работает, а другое нет, я не был доволен этим обходным решением.
И я узнаю разницу в моих двух приложениях.
Тот, у кого сломанный JavaScriptInterface, использует Proguard.
После небольшого поиска я нахожу решение .
Краткое описание: интерфейс JavascriptCallback, который реализован JavaScriptInterface и добавлены правила для Proguard в proguard.conf:
public interface JavascriptCallback {
}
public class JavaScriptInterface implements JavascriptCallback {
Context mContext;
/** Instantiate the interface and set the context */
JavaScriptInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
proguard.cfg:
-keep public class YOURPACKAGENAMEHERE.JavascriptCallback
-keep public class * implements YOURPACKAGENAMEHERE.JavascriptCallback
-keepclassmembers class * implements YOURPACKAGENAMEHERE.JavascriptCallback {
<methods>;
}
Ответ 2
Итак, я рад сказать, что моя проблема решена. В основном, это известная ошибка в Gingerbread и присутствует на моем устройстве 2.3.4. После некоторой царапины головы я нашел это обходное решение, придуманное Джейсоном Шахом в PhoneGap. Настоящая премия за это относится к нему, поскольку мое решение представляет собой слегка измененную версию кода в этом сообщении.
WebView
В моем методе onLoad я вызываю следующую функцию.
private void configureWebView() {
try {
if (Build.VERSION.RELEASE.startsWith("2.3")) {
javascriptInterfaceBroken = true;
}
} catch (Exception e) {
// Ignore, and assume user javascript interface is working correctly.
}
threadView = (WebView) findViewById(R.id.webViewThread);
threadView.setWebViewClient(new ThreadViewClient());
Log.d(APP_NAME, "Interface Broken? " + javascriptInterfaceBroken.toString());
// Add javascript interface only if it not broken
iface = new JavaScriptInterface(this);
if (!javascriptInterfaceBroken) {
threadView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
}
}
Здесь происходит несколько вещей.
-
В отличие от метода PhoneGap, я использую сравнение startsWith
с версией. Это связано с тем, что Build.VERSION.RELEASE 2.3.4 на моем ссылочном устройстве. Вместо того, чтобы тестировать все версии в серии 2.3, мне удобнее рисовать все устройства одним мазком.
-
javascriptInterface - это bool
, инициализированный до false
. JavaScriptInterface, созданный как iface, является классом, который обычно обрабатывает события JS в моем WebView.
-
ThreadViewClient - это мясо и картофель моей реализации. Это где вся логика для обработки обходного пути происходит.
WebViewClient
В классе ThreadViewClient (который расширяет WebViewClient), я в первую очередь учитываю тот факт, что обработчик js, который обычно подключается к Android, здесь отсутствует. Это означает, что если я хочу использовать одни и те же вызовы javascript из моего WebView, мне нужно дублировать интерфейс. Это достигается путем добавления пользовательских обработчиков в содержимое вашего сайта после его загрузки...
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (javascriptInterfaceBroken) {
final String handleGingerbreadStupidity =
"javascript:function shortSignature(id) { window.location='http://MyHandler:shortSignature:'+id; }; "
+ "javascript: function longSignature(text, username, forumnumber,threadnumber,pagenumber,postid) { var sep='[MyHandler]';"
+ "window.location='http://MyHandler:longSignature:' + encodeURIComponent(text + sep + username + sep + forumnumber + sep + threadnumber + sep + pagenumber + sep + postid);};"
+ "javascript: function handler() { this.shortSignature = shortSignature; this.longSignature = longSignature;}; "
+ "javascript: var Android = new handler();";
view.loadUrl(handleGingerbreadStupidity);
}
}
Там там много всего процесса. В javascript я определяю объект handler
, который содержит функции, которые отображаются в моем js-интерфейсе. Затем экземпляр его привязан к "Android", который является тем же именем интерфейса, что и для реализации, отличной от версии 2.3. Это позволяет повторно использовать код, отображаемый в вашем веб-просмотре.
Функции используют тот факт, что Android позволяет перехватывать всю навигацию, которая происходит в WebView. Чтобы общаться с внешней программой, они меняют расположение окна на одно со специальной подписью. Я займусь этим немного.
Еще одна вещь, которую я делаю, - это конкатенация параметров функций с несколькими параметрами. Это позволяет мне уменьшить сложность кода в обработчике местоположения.
Обработчик местоположения также помещается в ThreadViewClient...
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Method sMethod = null;
Log.d(APP_NAME, "URL LOADING");
if (javascriptInterfaceBroken) {
if (url.contains("MyHandler")) {
StringTokenizer st = new StringTokenizer(url, ":");
st.nextToken(); // remove the 'http:' portion
st.nextToken(); // remove the '//jshandler' portion
String function = st.nextToken();
String parameter = st.nextToken();
Log.d(APP_NAME, "Handler: " + function + " " + parameter);
try {
if (function.equals("shortSignature")) {
iface.shortSignature(parameter);
} else if (function.equals("longSignature")) {
iface.longSignature(parameter);
} else {
if (sMethod == null) {
sMethod = iface.getClass().getMethod(function, new Class[] { String.class });
}
sMethod.invoke(iface, parameter);
}
}
//Catch & handle SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
return true;
}
}
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
Здесь я перехватываю все события загрузки URL-адресов, которые происходят в WebView. Если целевой URL содержит волшебную строку, приложение пытается проанализировать ее, чтобы извлечь вызов метода. Вместо того, чтобы использовать токенизатор для извлечения отдельных параметров, я передаю его в версию моего метода longSignature
, который может анализировать и обрабатывать его. Это подробно описано в заключительной части этого сообщения.
Если к моменту выхода из блока "javascriptInterfaceBroken" выполнение не будет возвращено вызывающему, этот метод рассматривает действие загрузки URL как событие, связанное с обычной ссылкой. В случае моего приложения я не хочу использовать WebView для этого, поэтому я передаю его в операционную систему с помощью намерения ACTION_VIEW.
Это очень похоже на реализацию на блоге Джейсона. Тем не менее, я в большинстве своем обойдусь отражением. Я пытался использовать метод в блоке с отражением для обработки всех моих связанных функций, но из-за того, что мой JavaScriptInterface был вложенным классом, мне не удалось заглянуть в него из другого. Однако, поскольку я определил интерфейс в основной области действия, его методы можно вызвать напрямую.
Обработка конкатенированных параметров
Наконец, в моем JavaScriptInterface я создал обработчик для рассмотрения случая конкатенированного параметра...
public void longSignature(String everything) {
try {
everything = URLDecoder.decode(everything, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(APP_NAME, e);
}
final String[] elements = everything.split("\\[MyHandler\\]");
if (elements.length != 6) {
Toast.makeText(getApplicationContext(), "[" + elements.length + "] wrong number of parameters!", Toast.LENGTH_SHORT).show();
}
else {
longSignature(elements[0], elements[1], elements[2], elements[3], elements[4], elements[5]);
}
}
Урановый полиморфизм!
И это мое решение! Там много возможностей для улучшения, но на данный момент этого достаточно. Извините, если некоторые из моих соглашений подняли ваши взломы - это мое первое приложение для Android, и я не знаком с некоторыми из лучших практик и соглашений. Удачи!
Ответ 3
Вы должны аннотировать (@JavascriptInterface) методы в классе Java, которые вы хотите сделать доступными для JavaScript.
public class JavaScriptInterface {
Context context;
@JavascriptInterface
JavaScriptInterface(Context c) {
context = c;
}
@JavascriptInterface
public void edit(String postid) {
Log.d("myApp", "EDIT!");
//do stuff
} }
Это сработало для меня. Попробуйте это.
Ответ 4
Я применил реализацию Джейсона Шаха и г-на S в качестве строительного блока для моего исправления и значительно улучшил его.
В этот комментарий есть слишком много кода, который я просто свяжу с ним.
Ключевые моменты:
- Относится ко всем версиям Gingerbread (2.3.x)
- Звонки с JS на Android теперь синхронны
- Больше не нужно вручную отображать методы интерфейса.
- Исправлена возможность разбиения разделителей строк
- Намного легче изменить JS-подпись и имена интерфейсов