在Android WebView中捕获JS运动事件并触发系统通知
问题描述
我在连接运动传感器的ESP32微控制器上搭建了Web服务器,该服务器托管的网页会在运动传感器触发时生成JavaScript motion事件。我已在Android Studio中创建了加载该网页的WebView,现需实现捕获此JS motion事件,让WebView在事件每次触发时发送系统通知。以下是我编写的相关代码:
WebView代码(MainActivity.java)
package com.test.webviewsimulator; import android.annotation.SuppressLint; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.webkit.JavascriptInterface; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private WebView myWebView; private static final String CHANNEL_ID = "js_alert_channel"; private static final String URL = "http://192.168.1.108:8080"; // Static URL @SuppressLint("SetJavaScriptEnabled") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); createNotificationChannel(); myWebView = findViewById(R.id.webview); myWebView.setWebViewClient(new myWebClient()); myWebView.setWebChromeClient(new myWebChromeClient()); WebSettings webSettings = myWebView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); // Load the static URL myWebView.loadUrl(URL); Log.d(TAG, "Loading URL: " + URL); myWebView.addJavascriptInterface(new WebAppInterface(), "Android"); } private static class myWebClient extends WebViewClient { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); Log.d(TAG, "Page started loading: " + url); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); Log.d(TAG, "URL loading overridden: " + url); return true; } } private class myWebChromeClient extends WebChromeClient { @Override public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) { Log.d(TAG, "Console message: " + consoleMessage.message()); String message = consoleMessage.message(); if (message.startsWith("motion:")) { String motionValue = message.split(":")[1].trim(); Log.d(TAG, "Motion value: " + motionValue); if ("1".equals(motionValue)) { showNotification(); } } return super.onConsoleMessage(consoleMessage); } } @Override public void onBackPressed() { if (myWebView.canGoBack()) { myWebView.goBack(); } else { super.onBackPressed(); } } private void showNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.notification_icon) .setContentTitle("Motion Detected") .setContentText("Motion has been detected.") .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, 1); return; } notificationManager.notify(1, builder.build()); Log.d(TAG, "Notification shown"); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = "JavaScript Alert Channel"; String description = "Channel for JavaScript Alert notifications"; int importance = NotificationManager.IMPORTANCE_HIGH; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.setDescription(description); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } public class WebAppInterface { @JavascriptInterface public void log(String message) { Log.d(TAG, "JavaScript interface log message: " + message); if ("motion: 1".equals(message)) { showNotification(); } } } }
布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="732dp" android:layout_marginTop="10dp" /> </RelativeLayout>
Java模拟服务器代码
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.OutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; public class Main { public static void main(String[] args) throws Exception { // 创建HTTP服务器,监听指定IP和端口 InetAddress localAddress = InetAddress.getByName("0.0.0.0"); HttpServer server = HttpServer.create(new InetSocketAddress(localAddress, 8080), 0); // 为服务器创建默认上下文 server.createContext("/", new HtmlHandler()); server.createContext("/events", new EventHandler()); // 创建线程池处理请求 ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); server.setExecutor(threadPoolExecutor); // 启动服务器 server.start(); System.out.println("服务器正在运行..."); } static class HtmlHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { // 记录连接尝试 LocalDateTime dataTimeStamp = LocalDateTime.now(); String currentTime = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss").format(dataTimeStamp); System.out.println("连接来源: " + exchange.getRemoteAddress() + " 时间: " + currentTime); // 设置响应内容类型为HTML exchange.getResponseHeaders().set("Content-Type", "text/html"); // HTML页面内容 String response = "<!DOCTYPE HTML><html>" + "<head><title>ESP Web Server</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></head>" + "<body><h2>ESP Web Server</h2><script>" + "if (!!window.EventSource) {" + "var source = new EventSource('/events');" + "source.addEventListener('open', function(e) { console.log(\"Events Connected\"); }, false);" + "source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log(\"Events Disconnected\"); } }, false);" + "source.addEventListener('message', function(e) { console.log(\"message\", e.data); }, false);" + "source.addEventListener('motion', function(e) { console.log(\"motion\", e.data); }, false);" + "}" + "</script></body></html>"; // 发送响应头和内容 exchange.sendResponseHeaders(200, response.getBytes().length); OutputStream outputStream = exchange.getResponseBody(); outputStream.write(response.getBytes()); outputStream.close(); } } static class EventHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { // 设置SSE响应内容类型 exchange.getResponseHeaders().set("Content-Type", "text/event-stream"); exchange.getResponseHeaders().set("Cache-Control", "no-cache"); exchange.getResponseHeaders().set("Connection", "keep-alive"); // 发送响应头 exchange.sendResponseHeaders(200, 0); OutputStream outputStream = exchange.getResponseBody(); PrintWriter writer = new PrintWriter(outputStream, true); // 模拟运动事件 new Thread(() -> { try { while (true) { Thread.sleep(10000); String motionEvent = "motion: " + (Math.random() < 0.5 ? "0" : "1"); writer.println("event: motion"); writer.println("data: " + motionEvent); writer.println(); writer.flush(); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } }
修正方案
当前代码存在两处核心问题,导致无法正确捕获motion事件并触发通知,调整如下:
1. 修改网页端JavaScript逻辑
当前网页仅在控制台打印motion事件,未主动通知Android端。需修改模拟服务器中HtmlHandler的HTML内容,在motion事件回调里调用Android的JavascriptInterface方法:
// 替换HtmlHandler中motion事件监听代码: source.addEventListener('motion', function(e) { console.log("motion", e.data); // 调用Android端暴露的方法 if (typeof Android !== 'undefined') { Android.log(e.data); } }, false);
2. 修正Android端控制台监听逻辑
当前控制台输出的消息格式为motion motion: 1,原代码中message.startsWith("motion:")无法匹配,可调整为:
// 修改myWebChromeClient的onConsoleMessage方法中的判断: if (message.contains("motion: 1")) { showNotification(); }
3. 补充权限配置
在AndroidManifest.xml中添加Android 13+所需的通知权限:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
内容的提问来源于stack exchange,提问作者Med Yassine Aghnaj
相关产品推荐
相关产品推荐

