You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 03:37:37