我感谢 Shai 的建议,但它们不足以获得工作代码和工作模拟器(在其中按 Esc 没有任何作用,而且我不理解在这种情况下使用断点的含义)。只是在浏览器组件上调用back() 不是我需要的,因为我需要使用更复杂的方法,即调用我编写的javascript 函数,使浏览器只有在Internet 连接可用时才返回(或者它显示一条消息,表明 Internet 不可用)。
所以,这就是我尝试自己回答问题的原因,将问题分解为几个步骤。我花了几天时间和很多精力来解决一个如此简单的问题(调用 javascript 按下后退按钮),主要是因为缺乏文档(Codename One JS Bridge package 包含 API 中的几个示例,但不是这种情况)以及 Codename One 模拟器的崩溃(和奇怪的行为),如下文所述。
我希望帮助其他人分享我所做的事情。
第 1 步:使用 BrowserComponent 加载本地页面
我用 Netbeans 创建了一个新的空 Codename One 项目,我给它命名为“myBrowser”,包名“it.galgani.demos.mybrowser”,主类名“MyBrowser”,主题“native”,模板“Hello”世界(裸露的骨头)”。
我修改了默认的start() 方法:
public void start() {
if(current != null){
current.show();
return;
}
Form hi = new Form("MyBrowser", new BorderLayout());
// Create the BrowserComponent
BrowserComponent browser = new BrowserComponent();
try {
// Load a web page placed in the "html" package
browser.setURLHierarchy("/index.html");
Log.p("setURLHierarchy executed");
} catch (IOException ex) {
Log.e(ex);
}
hi.add(BorderLayout.CENTER, browser);
hi.show();
}
然后我在“源包”中创建了一个名为“html”的新“Java 包”。在我的情况下,创建的文件夹是:“/home/francesco/NetBeansProjects/myBrowser/src/html”
在 html 文件夹中,我创建了这个index.html(选择“新建文件”,类别“其他”,文件类型“HTML”,文件名“索引”):
<!DOCTYPE html>
<html>
<head>
<title>BrowserComponent setURLHierarchy test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>Hello world</div>
</body>
</html>
然后我点击“清理和构建项目”(没有错误,很好),然后点击“运行项目”:它工作正常,我的“Hello world”显示正确。现在下一步...
第 2 步:创建其他 html 页面
我将 index.html 修改为:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<a href="page1.html">Page 1</a><br />
<a href="page2.html">Page 2</a><br />
<a href="page3.html">Page 3</a><br />
</body>
</html>
然后我用这段代码创建了page1.html、page2.html和page3.html(根据页面改变h1):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Page 1</h1>
</body>
</html>
我在模拟器中都试过了,没问题。要从一个页面返回到上一个页面,在模拟器中有一个选项可以通过在页面上单击鼠标右键来访问。
日志报告了两个java异常,第一个是模拟器打开的时候,第二个是关闭的时候,但是不影响模拟器的功能:
java.io.UTFDataFormatException: malformed input around byte 64
at java.io.DataInputStream.readUTF(DataInputStream.java:656)
at java.io.DataInputStream.readUTF(DataInputStream.java:564)
at com.codename1.ui.util.Resources.loadTheme(Resources.java:1270)
at com.codename1.ui.util.Resources.openFileImpl(Resources.java:303)
at com.codename1.ui.util.Resources.openFile(Resources.java:269)
at com.codename1.ui.util.Resources.<init>(Resources.java:189)
at com.codename1.ui.util.Resources.open(Resources.java:768)
at com.codename1.ui.util.Resources.open(Resources.java:688)
at com.codename1.impl.javase.JavaSEPort$4.run(JavaSEPort.java:1720)
at com.codename1.ui.Display.processSerialCalls(Display.java:1056)
at com.codename1.ui.Display.mainEDTLoop(Display.java:873)
at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
[EDT] 0:0:1,396 - Codename One revisions: 14b404a993fcb91cfe25e26ce4d64ee044952b39
[EDT] 0:0:1,444 - setURLHierarchy executed
Exception in thread "JavaFX Application Thread" java.lang.IncompatibleClassChangeError
at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
at com.sun.glass.ui.gtk.GtkApplication.lambda$null$49(GtkApplication.java:139)
at java.lang.Thread.run(Thread.java:748)
唯一真正的坏事是文本在使用 iPhone3gs.skin 时具有可读的大小,但在使用其他皮肤(如 SamsungS7.skin)时却非常小(非常不可读)。我不知道为什么,我希望文本在真实设备上的大小是正确的,并且具有可读性好的默认字体大小。
第 3 步:将命令与返回按钮关联
在这种情况下,为了简化和测试后退按钮,我没有使用 Javascript Bridge,而是使用 BrowserComponent 类的 back() 方法。我在接下来的步骤中尝试了 Javascript Bridge。我在 MyBrowser 的start() 方法的末尾(就在hi.show() 之前)添加了这段代码:
hi.setBackCommand(new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
browser.back();
}
});
在模拟器中,硬件后退按钮是非反应性的,并且工具栏中没有显示后退箭头。经过几次尝试,我意识到setBackCommand方法是在Form类和Toolbar类中实现的,行为不同。这篇文章解释了区别:
https://www.codenameone.com/blog/toolbar-back-easier-material-icons.html
以下是此步骤的最终代码,在模拟器和我的真实 Android 设备(我在其中测试了硬件后退按钮和材料图标后退箭头)中都能正常工作:
public void start() {
if (current != null) {
current.show();
return;
}
Form hi = new Form("MyBrowser", new BorderLayout());
// Create the BrowserComponent
BrowserComponent browser = new BrowserComponent();
try {
// Load a web page placed in the "html" package
browser.setURLHierarchy("/index.html");
Log.p("setURLHierarchy executed");
} catch (IOException ex) {
Log.e(ex);
}
hi.add(BorderLayout.CENTER, browser);
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
browser.back();
}
};
hi.getToolbar().setBackCommand(backCommand);
hi.show();
}
第 4 步:在 Javascript Bridge 中使用后退按钮
我将backCommand 更改为如下调用一个简单的writeln 函数,但新代码只运行了几次,其他时候它使模拟器崩溃或者它什么也没做:
// Javascript Bridge
JavascriptContext context = new JavascriptContext(browser);
JSObject document = (JSObject)context.get("document");
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
// browser.back();
document.call("writeln", new Object[]{"Hello world"});
}
};
我怀疑 JavascriptContext 始终未正确加载,但我不确定。日志对理解问题没有帮助:
[EDT] 0:0:0,18 - setURLHierarchy executed
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007f3a04f944d1, pid=3730, tid=0x00007f3a07cdf700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_144-b01) (build 1.8.0_144-b01)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# C [libjfxwebkit.so+0x11334d1] checkJSPeer(long, int, OpaqueJSValue*&, OpaqueJSContext const*&)+0x41
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /home/francesco/NetBeansProjects/myBrowser/hs_err_pid3730.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
Java Result: 134
经过(非常长的)尝试和错误列表来解决这种奇怪的行为,最后我以在模拟器和我的真实 Android 设备中正常工作的方式更改了代码(请注意,我不再使用JavascriptContext 类):
public void start() {
if (current != null) {
current.show();
return;
}
Form hi = new Form("MyBrowser", new BorderLayout());
// Create the BrowserComponent
BrowserComponent browser = new BrowserComponent();
try {
// Load a web page placed in the "html" package
browser.setURLHierarchy("/index.html");
Log.p("setURLHierarchy executed");
} catch (IOException ex) {
Log.e(ex);
}
hi.add(BorderLayout.CENTER, browser);
// Create a command for the Back button
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
browser.execute("document.writeln('Hello world!');");
}
};
hi.getToolbar().setBackCommand(backCommand);
hi.show();
}
诀窍是使用 BrowserComponent 的(很少记录)execute 方法,而不是 JavascriptContext + JSObject 类。 StackOverflow 问题中对此方法的简要说明:Clarification on Codename One's BrowserComponent execute(String javaScript) method。
第 5 步:使用我的 goBackButton() javascript 函数
我已经在我打算在我的应用程序中使用的网页中实现了goBackButton() 函数。我只想在 javascript 引擎已加载该方法时才调用该方法,因为:«BrowserComponent.execute(String) 将在您进行调用时在浏览器的当前页面中执行提供 JS sn-p。如果您的 sn-p 引用了尚未加载的内容,则 javascript 将导致错误。» (citation)
这就是我更改代码的原因:
// Create a command for the Back button
String jsCode = "function goBack() { "
+ " if (typeof goBackButton == 'function') { "
+ " window.goBackButton(); "
+ " return 'I\\'m invoking goBackButton()'; "
+ " } else { "
+ " window.history.go(-1); "
+ " return 'goBackButton() is not available';"
+ " } "
+ "} "
+ "goBack(); ";
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
Log.p(browser.executeAndReturnString(jsCode));
}
};
为了做一个基本的测试,我在 page1.html 中实现了一个假的 goBackButton():
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
function goBackButton() {
// My js code
// ...
history.go(-1);
}
</script>
</head>
<body>
<h1>Page 1</h1>
</body>
</html>
然后我点击 page1.html 和 page2.html 上的后退按钮。结果日志符合预期:
[EDT] 0:0:0,28 - setURLHierarchy executed
[EDT] 0:0:4,395 - I'm invoking goBackButton()
[EDT] 0:0:7,736 - goBackButton() is not available
第6步:使用setBrowserNavigationCallback
我使用setBrowserNavigationCallback 只允许在我的域内浏览:在我原来的错误代码中,我体验到如果我使用JavascriptContext + JSObject 类,那么我必须使用条件if(url.startsWith("javascript")),因为通过点击后退按钮调用 NavigationCallback(不要问我为什么,我不知道)。相反,使用BrowserComponent.execute(String) 我不需要检查 url 是否以“javascript”开头,因为 NavigationCallback 永远不会被 javascript 调用。
我添加了两个指向 index.html 的链接来做一些检查:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<a href="page1.html">Page 1</a><br />
<a href="page2.html">Page 2</a><br />
<a href="page3.html">Page 3</a><br />
<a href="https://www.google.com/">WebPage 1 (domain not allowed)</a><br />
<a href="https://www.utiu-students.net/">WebPage2 (domain allowed)</a>
</body>
</html>
下面的代码看起来是正确的,但实际上每次我点击“WebPage 1”链接(即谷歌页面)时它都会使模拟器崩溃:
public void start() {
if (current != null) {
current.show();
return;
}
Form hi = new Form("MyBrowser", new BorderLayout());
// Create the BrowserComponent
BrowserComponent browser = new BrowserComponent();
try {
// Load a web page placed in the "html" package
browser.setURLHierarchy("/index.html");
Log.p("setURLHierarchy executed");
} catch (IOException ex) {
Log.e(ex);
}
hi.add(BorderLayout.CENTER, browser);
// Create a command for the Back button
String jsCode = "function goBack() { "
+ " if (typeof goBackButton == 'function') { "
+ " window.goBackButton(); "
+ " return 'I\\'m invoking goBackButton()'; "
+ " } else { "
+ " window.history.go(-1); "
+ " return 'goBackButton() is not available';"
+ " } "
+ "} "
+ "goBack(); ";
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
Log.p(browser.executeAndReturnString(jsCode));
}
};
hi.getToolbar().setBackCommand(backCommand);
// Allow browsing only inside my domains
String myDomain1 = "127.0.0.1"; // for local server (using simulator)
String myDomain2 = "utiu-students.net";
String myDomain3 = "login.uninettunouniversity.net";
browser.setBrowserNavigationCallback((url) -> {
Log.p("URL loaded: " + url);
String domain = ""; // the domain of the url loaded by BrowserComponent
if (url.startsWith("http")) {
try {
domain = (new URI(url)).getHost();
domain = domain.startsWith("www.") ? domain.substring(4) : domain;
} catch (URISyntaxException ex) {
Log.e(ex);
}
}
Log.p("Domain of the URL: " + domain);
if (url.startsWith("file")
|| domain.equals(myDomain1)
|| domain.equals(myDomain2)
|| domain.equals(myDomain3)) {
Log.p("the BrowserComponent can navigate");
return true; // the BrowserComponent can navigate
} else {
Display.getInstance().callSerially(() -> {
// it opens the url with the native platform
Boolean can = Display.getInstance().canExecute(url);
if (can != null && can) {
Display.getInstance().execute(url);
}
});
Log.p("the BrowserComponent cannot navigate");
return false; // the BrowserComponent cannot navigate
}
});
hi.show();
}
这是日志:
[JavaFX Application Thread] 0:0:2,86 - URL loaded: https://www.google.com/
[JavaFX Application Thread] 0:0:2,87 - Domain of the URL: google.com
[JavaFX Application Thread] 0:0:2,88 - the BrowserComponent cannot navigate
[JavaFX Application Thread] 0:0:3,815 - URL loaded: https://www.google.it/?gfe_rd=cr&ei=avGmWa-YNKnS8AfqlIGoDw
[JavaFX Application Thread] 0:0:3,815 - Domain of the URL: google.it
[JavaFX Application Thread] 0:0:3,815 - the BrowserComponent cannot navigate
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007f3ec48044cf, pid=5754, tid=0x00007f3ec7be4700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_144-b01) (build 1.8.0_144-b01)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# C [libjfxwebkit.so+0xa9e4cf] WebCore::ResourceLoader::willSendRequestInternal(WebCore::ResourceRequest&, WebCore::ResourceResponse const&)+0x53f
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /home/francesco/NetBeansProjects/myBrowser/hs_err_pid5754.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
Java Result: 134
这很糟糕。奇怪的是谷歌页面被加载了两次,我猜是因为重定向。如果将 Google 的链接替换为 Codename One 网站的链接,模拟器会停止崩溃……但它会打开页面!所以BrowserNavigationCallBack() 不起作用。
因为这个问题,我做了两次检查。
第一个是对原生浏览器的支持:
// Check if the native browser is supported (only for logging)
if (BrowserComponent.isNativeBrowserSupported()) {
Log.p("Native Browser Supported");
} else {
Log.p("Native Browser NOT Supported");
}
第二个是删除BrowserNavigationCallBack()的代码并替换为:
browser.setBrowserNavigationCallback((new BrowserNavigationCallback() {
@Override
public boolean shouldNavigate(String url) {
Log.p("URL: " + url);
return false;
}
}));
糟糕的结果是 CodenameOne 网站仍然打开:shouldNavigate 的返回值被模拟器忽略(这是一个错误吗?)。这是日志:
[EDT] 0:0:0,44 - Native Browser Supported
[EDT] 0:0:0,59 - setURLHierarchy executed
[JavaFX Application Thread] 0:0:0,90 - URL: file:///home/francesco/NetBeansProjects/myBrowser/build/classes/html/index.html
[JavaFX Application Thread] 0:0:2,535 - URL: https://www.codenameone.com/
但是,经过几次尝试,我找到了一个小解决方法,我认为它适用于大多数用例。
我在setBrowserNavigationCallback末尾添加了这段代码:
Log.p("the BrowserComponent cannot navigate");
if (Display.getInstance().isSimulator()) {
// small workaround because the return value is ignored by the simulator
browser.setURL("jar:///goback.html");
}
return false; // the BrowserComponent cannot navigate
我把goback.html放在了html包的外面,在/src:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>window.history.go(-1);</script>
</head>
<body>
The link will be opened with an external browser.<br />
<a href="javascript:window.history.go(-1);">Click here to go back</a>
</body>
</html>
现在模拟器(显然)可以正常工作:它似乎没有打开指向不允许域的链接......实际上它打开了很短的时间,然后自动返回(并以外部浏览器,如我所愿)。
还有一个非常好的消息:你还记得点击下面的链接会导致模拟器崩溃吗(我报告了上面的日志)?我的解决方法也解决了这个问题:没有崩溃,没有奇怪的行为,并且链接是在外部浏览器中打开的。
<a href="https://www.google.com/">WebPage 1 (domain not allowed)</a><br />
该应用程序在我真正的 Android 设备上也能正常运行。
这是最终代码:
public void start() {
if (current != null) {
current.show();
return;
}
Form hi = new Form("MyBrowser", new BorderLayout());
// Create the BrowserComponent
BrowserComponent browser = new BrowserComponent();
// Check if the native browser is supported (only for logging)
if (BrowserComponent.isNativeBrowserSupported()) {
Log.p("Native Browser Supported");
} else {
Log.p("Native Browser NOT Supported");
}
try {
// Load a web page placed in the "html" package
browser.setURLHierarchy("/index.html");
Log.p("setURLHierarchy executed");
} catch (IOException ex) {
Log.e(ex);
}
hi.add(BorderLayout.CENTER, browser);
// Create a command for the Back button
String jsCode = "function goBack() { "
+ " if (typeof goBackButton == 'function') { "
+ " window.goBackButton(); "
+ " return 'I\\'m invoking goBackButton()'; "
+ " } else { "
+ " window.history.go(-1); "
+ " return 'goBackButton() is not available';"
+ " } "
+ "} "
+ "goBack(); ";
Command backCommand = new Command("BackButton") {
@Override
public void actionPerformed(ActionEvent evt) {
Log.p(browser.executeAndReturnString(jsCode));
}
};
hi.getToolbar().setBackCommand(backCommand);
// Allow browsing only inside my domains
String myDomain1 = "127.0.0.1"; // for local server (using simulator)
String myDomain2 = "utiu-students.net";
String myDomain3 = "login.uninettunouniversity.net";
browser.setBrowserNavigationCallback((url) -> {
Log.p("URL loaded: " + url);
String domain = ""; // the domain of the url loaded by BrowserComponent
if (url.startsWith("http")) {
try {
domain = (new URI(url)).getHost();
domain = domain.startsWith("www.") ? domain.substring(4) : domain;
} catch (URISyntaxException ex) {
Log.e(ex);
}
}
Log.p("Domain of the URL: " + domain);
if (url.startsWith("file")
|| domain.equals(myDomain1)
|| domain.equals(myDomain2)
|| domain.equals(myDomain3)) {
Log.p("the BrowserComponent can navigate");
return true; // the BrowserComponent can navigate
} else {
Display.getInstance().callSerially(() -> {
// it opens the url with the native platform
Boolean can = Display.getInstance().canExecute(url);
if (can != null && can) {
Display.getInstance().execute(url);
}
});
Log.p("the BrowserComponent cannot navigate");
if (Display.getInstance().isSimulator()) {
// small workaround because the return value is ignored by the simulator
browser.setURL("jar:///goback.html");
}
return false; // the BrowserComponent cannot navigate
}
}
);
hi.show();
}
作为参考,这个(非常小的)浏览应用程序的最终完整源代码在这里:
https://github.com/jsfan3/CodenameOne-Apps/tree/master/Browsing/myBrowser
第 7 步:使用真实网页内容测试应用
现在是时候使用真实内容测试应用了。我删除了 html 包中的文件并复制了我想要使用的文件。结果:我的 javascript 代码在模拟器中没有按预期运行(可能是因为 JavaFX 限制),但是该应用程序最初在我的真实 Android 设备中按预期运行良好。然后,几分钟后,硬件返回按钮停止工作,并且材料左箭头图标消失了......非常沮丧,杀死应用程序并重新启动手机并没有恢复返回按钮:我不得不删除并重新安装应用程序让它再次工作(我将在另一集中调查这个问题,目前我无法复制它......)。