【TopDesk】3.1.2. 利用JNI在Java中检测耳机插拔

来源:互联网 发布:网络情感电台结束语 编辑:程序博客网 时间:2024/06/10 03:08

以前折腾视频编码的时候总是感叹,为什么这帮人拿着这么高的工资写出来的标准却这么乱七八糟的,直到现在才知道:任何一个领域,当你深入到他最底层的部分时,他一定是杂乱无章而充斥着垃圾的。上层的干净整洁相貌堂堂,都是靠这这些七扭八歪的柱子撑起来的。

0x02 实践是检验真理的唯一标准

不知道我的读者中有多少是写Java的,也不知道有多少是写过JNI的,无论如何,一个简单的介绍大概算不上什么坏事。

JNI,全称Java Native Interface,即Java本地接口。它处于JVM并列的位置上,为Java程序与本地程序互相协作(也就是windows上的dll/linux上的so/mac上的dylib文件,学名动态链接库)提供双向的接口——在c++函数里面调用Java方法,或者在Java类方法里面调用dll提供的函数。

当然JNI的主要目的是用其他语言来服务Java程序,Java处于主导地位,原因有二:

  1. 本地程序只能动态加载,程序入口在Java程序中。只提供了在Java程序中加载一个库的方法,没有提供在c++程序中启动一个JVM的方法。(待查证)
  2. 双方之间传递数据必须使用JNI提供的Java类型,传统的指针/字面量必须转换为Java类型才能传递,这一点在JNI提供的各种函数的签名中也有体现。

这就导致了,在Java中声明要调用一个外部的函数非常简单——在方法签名中加上native关键字,并且在程序入口处加载一下动态链接库即可。此后Java文件也可直接通过编译,甚至打包成为JAR文件都与正常流程完全相同,只有在运行时执行到了本地方法却没有加载相应的库时会报错。

与之相反地,JNI的本地库一侧的编程则相对麻烦得多——首先要编译Java代码到字节码的.class文件,然后利用JDK中的javah命令生成对应改Java类的c/c++头文件,在编写完成c/c++实现之后需要编译生成动态链接库文件(dll/so/dyilb),最后在启动JVM的命令行中把动态链接库的幕加入java.library.path系统变量中才算真正完成。

其中光是编写c/c++实现这一点就极为复杂:比如一个本地方法

public native String foo(String str);

实现把一个字符串倒序的功能(当然大脑正常的人多半都会选择直接用Java实现),除了正常的操作char *以外,还得加上多出来的两步——jstring转换到char *再转回来。
要是程序中用到了win32 API则更加麻烦,众所周知,windows中为了兼容unicode所使用的字符串格式不是单字节的char *而是双字节的LPWSTR,或者wchar_t *

当然这篇文章并不是为了介绍JNI的,按照标题重点应该在“利用JNI”上,也自然不打算系统性地讲解JNI编程,有兴趣的道友可以自行百度。

之所以一上来就扯了一千来字,是为了抒发在实现这个功能的过程中内心积蓄的苦闷,再结合开篇的那段话——上层的干净整洁相貌堂堂,都是靠这这些七扭八歪的柱子撑起来的——而我们现在就要钻进这堆烂柱子中一探究竟。

0x03 建设有中国特色的社会主义

在前篇的最后我们写出了一个能够监听到耳机插拔事件的c++ Demo,下一步任务自然便是把他变成Java可以使用的代码。

注:本节按照当初的开发步骤,弯路走了不少,懒得看的可以直接拉到最后成品。

前面提到利用IMMNotificationClient中OnPropertyValueChanged回调函数可以监听到耳机插拔的事件,然而只能通过终端设备的名称以“Internal”还是“External”开头来判断,还得过滤掉名称为空的事件,这显然不能直接拿来用。
所幸不管是Internal还是External的事件都是一连串地来的,因此我们可以这样设计逻辑:程序中维护一个当前插拔状态的变量,当新的事件到来时更新这个变量,Internal开头的赋false(已拔出),External开头的赋true(已插入),都不是的不改变,然后只有当这个值真正改变(修改前后不同)时才通知拔出/插入。

由于JNI程序调试起来很不方便,这个逻辑也打算放在Java里面来实现,直接上代码:

package io.github.std4453.topdesk.headphone;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * */public class HeadphonePeer {    private List<HeadphoneEventListener> listeners ;    private boolean inserted ;    private ExecutorService executor;    public HeadphonePeer() throws Exception {        this.inserted = false;        this.listeners = new ArrayList<>();        this.executor = Executors.newSingleThreadExecutor();        this.startListening();    }    public void addListener(HeadphoneEventListener listener) {        this.listeners.add(listener);    }    public void onEvent(String name) {        System.out.println(name);        if (name.startsWith("External")) this.onInsert();        else if (name.startsWith("Internal")) this.onRemove();    }    public synchronized void onInsert() {        if (!this.inserted) {            this.inserted = true;            this.executor.submit(() -> this.listeners.forEach                    (HeadphoneEventListener::onHeadphoneInserted));        }    }    private synchronized void onRemove() {        if (this.inserted) {            this.inserted = false;            this.executor.submit(() -> this.listeners.forEach                    (HeadphoneEventListener::onHeadphoneRemoved));        }    }    private void startListening() throws Exception {        String msg = nStartListening();        if (msg != null) throw new Exception(msg);    }    void stopListening() throws Exception {        this.nStopListening();    }    private native String nStartListening();    private native void nStopListening();}

注:onEvent()方法用于c++部分回调,传递的参数就是前面提到的终端设备名称。stopListening()方法是为了显式析构监听器,防止内存泄漏,因为Java的finalize()函数并不保证一定调用。nStartListening()返回null以表示成功,否则返回错误信息。

package io.github.std4453.topdesk.headphone;/** * */public interface HeadphoneEventListener {    void onHeadphoneInserted();    void onHeadphoneRemoved();}
package io.github.std4453.topdesk.headphone;/** * */public class HeadphoneTest {    public static void main(String[] args) throws Exception{        System.loadLibrary("topdesk");        HeadphonePeer peer = new HeadphonePeer();        peer.addListener(new HeadphoneEventListener() {            @Override            public void onHeadphoneInserted() {                System.out.println("Headphone inserted!");            }            @Override            public void onHeadphoneRemoved() {                System.out.println("Headphone removed!");            }        });        System.out.println("Press any key to exit.");        System.in.read();        peer.stopListening();    }}

Java部分并不复杂,麻烦的是c++的部分。
javah命令导出生成的.h文件就不贴了,都是自动生成的没有修改。对应的实现一开始自然是直接修改之前讲的demo,代码如下:

#define SAFE_RELEASE(punk) \ if ((punk) != NULL) \ { (punk)->Release(); (punk) = NULL; }   #include <stdlib.h>#include <stdio.h>#include <windows.h>#include <setupapi.h>  #include <initguid.h>#include <mmdeviceapi.h>  #include <Functiondiscoverykeys_devpkey.h>#include "io_github_std4453_topdesk_headphone_HeadphonePeer.h"void onEvent(JNIEnv *env, jobject obj, LPWSTR str);class CMMNotificationClient: public IMMNotificationClient {   public:      IMMDeviceEnumerator *m_pEnumerator;      CMMNotificationClient(JNIEnv *env, jobject obj): _cRef(1), m_pEnumerator(NULL), env(env), obj(obj) {        // initialize COM        ::CoInitialize(NULL);          HRESULT hr = S_OK;           // create interface        hr = CoCreateInstance(            __uuidof(MMDeviceEnumerator), NULL,               CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),               (void**)&m_pEnumerator);           // if (hr!=S_OK) cout<<"Unable to create interface"<<endl;           // register event        hr = m_pEnumerator->RegisterEndpointNotificationCallback((IMMNotificationClient*)this);        // if (hr==S_OK) cout<<"注册成功"<<endl;           // else cout<<"注册失败"<<endl;       }      ~CMMNotificationClient() {          SAFE_RELEASE(m_pEnumerator)        ::CoUninitialize();    }    // IUnknown methods -- AddRef, Release, and QueryInterface   private:      LONG _cRef;    JNIEnv *env;    jobject obj;    // Private function to print device-friendly name    HRESULT _PrintDeviceName(LPCWSTR pwstrId) {        HRESULT hr = S_OK;        IMMDevice *pDevice = NULL;        IPropertyStore *pProps = NULL;        PROPVARIANT varString;        CoInitialize(NULL);        PropVariantInit(&varString);        hr = m_pEnumerator -> GetDevice(pwstrId, &pDevice);        // if (hr != S_OK) ; // throw "Unable to get device"        hr = pDevice -> OpenPropertyStore(STGM_READ, &pProps);        // if (hr != S_OK) ; // throw "Unable to opt property store"        // Get the endpoint device's friendly-name property.        hr = pProps -> GetValue(PKEY_Device_FriendlyName, &varString);        // return varString.pwszVal        printf("%S\n", varString.pwszVal);        fflush(stdout);        onEvent(env, obj, varString.pwszVal);        PropVariantClear(&varString);        SAFE_RELEASE(pProps)        SAFE_RELEASE(pDevice)        // return hr;    }    ULONG STDMETHODCALLTYPE AddRef() {          return InterlockedIncrement(&_cRef);      }      ULONG STDMETHODCALLTYPE Release() {          ULONG ulRef = InterlockedDecrement(&_cRef);          if (0 == ulRef) delete this;          return ulRef;      }      HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) {          if (IID_IUnknown == riid) {              AddRef();              *ppvInterface = (IUnknown*)this;          } else if (__uuidof(IMMNotificationClient) == riid) {              AddRef();              *ppvInterface = (IMMNotificationClient*)this;          } else {              *ppvInterface = NULL;              return E_NOINTERFACE;          }          return S_OK;      }      HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {return S_OK;}       HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) {           _PrintDeviceName(pwstrDeviceId);        return S_OK;       }   };jstring w2js(JNIEnv *env, LPWSTR src){    int src_len = wcslen(src);    jchar * dest = new jchar[src_len + 1];    memset(dest, 0, sizeof(jchar) * (src_len + 1));    for(int i = 0; i < src_len; i++)        memcpy(&dest[i], &src[i], 2);    jstring dst = env -> NewString(dest,src_len);    delete [] dest;    return dst;}void onEvent(JNIEnv *env, jobject obj, LPWSTR str) {    jstring jstr = w2js(env, str);    printf("jstr converted\n");    fflush(stdout);    jclass dpclazz = env -> FindClass("io/github/std4453/topdesk/headphone/HeadphonePeer");    printf("dpclazz %d\n", dpclazz);    fflush(stdout);    jmethodID method1 = env -> GetMethodID(dpclazz, "onEvent", "(Ljava/lang/String;)V");    printf("method1 %d\n", method1);    fflush(stdout);    env -> CallObjectMethod(obj,method1, jstr);}CMMNotificationClient *client;JNIEXPORT jstring JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStartListening(JNIEnv * env, jobject obj) {    client = new CMMNotificationClient(env, obj);    return NULL;}JNIEXPORT void JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStopListening(JNIEnv * env , jobject obj) {    delete client;}

注:开发中暂时注释掉了错误处理,之后再加。主要的改动就是加了对应Java里面的两个方法,然后把终端设备的名称不是直接打印而是传递给onEvent()函数,进一步给Java方法。

一切都看起来很美好(jni.h找不到的问题问度娘),编译生成dll,放到路径里面运行,然后:

运行结果

0xC00000005是什么意思呢,查了一下是Access Violation,根据代码里面的log只输出到jstr converted这一行判断,问题是出在了env -> FindClass(...)这里。
然而到底出了什么问题呢?我自信类名还没有打错,这个调用Java方法的流程也是按照教程一步一步来的完全没有问题,可事实就是,这一句代码导致了整个程序的崩溃。

原因就在于,多线程。

0x04 事物发展的曲折性和前进性

踩坑是变强的必经之路。

不管这句话是不是真的,多线程是在这项目的JNI编写过程中踩到的第一个坑,是不是最后一个还有待时间来验证。

还记得用IMMNotificationClient来监听事件的步骤是向一个IMMDeviceEmulator注册自己作为回调,而非轮询或者等待-唤醒等等其他的事件机制,然而回调的一个特点就是:回调函数的调用完全可能在另一个线程中。

这本来应该是一个优点,实际上我在Java部分的逻辑中也使用了这种机制,监听事件不会阻塞启动监听器的线程,只在有事件到达时用另外一个线程异步处理事件,然而在这里却成了一个极大的麻烦——据JNI文档中:

The JNIEnv pointer, passed as the first argument to every native method, can only be used in the thread with which it is associated. It is wrong to cache the JNIEnv interface pointer obtained from one thread, and use that pointer in another thread.

翻译一下:

作为每一个本地方法的第一个实参的JNIEnv指针只能在被调用的线程中使用。程序不应该缓存某一个线程中获得的该指针然后在另一个线程中使用它。

之前的程序崩溃的原因也正在于此。本来一厢情愿地在nStartListener()里面将env作为构造器参数传递给CMMNotificationClient,结果由于事件回调的异步,真正用到的时候这个env已经不能用了。

幸好这个问题早已有人研究过:JNI(Java Native Interface)在多线程中的运用。根据里面说的把代码改成:

#define SAFE_RELEASE(punk) \ if ((punk) != NULL) \ { (punk)->Release(); (punk) = NULL; }   #include <stdlib.h>#include <stdio.h>#include <windows.h>#include <setupapi.h>  #include <initguid.h>#include <mmdeviceapi.h>  #include <Functiondiscoverykeys_devpkey.h>#include "io_github_std4453_topdesk_headphone_HeadphonePeer.h"void onEvent(JNIEnv *env, jobject obj, LPWSTR str);class CMMNotificationClient: public IMMNotificationClient {   public:      IMMDeviceEnumerator *m_pEnumerator;      CMMNotificationClient(JavaVM *vm, jobject obj): _cRef(1), m_pEnumerator(NULL), vm(vm), obj(obj) {        // initialize COM        ::CoInitialize(NULL);          HRESULT hr = S_OK;           // create interface        hr = CoCreateInstance(            __uuidof(MMDeviceEnumerator), NULL,               CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),               (void**)&m_pEnumerator);           // if (hr!=S_OK) cout<<"Unable to create interface"<<endl;           // register event        hr = m_pEnumerator->RegisterEndpointNotificationCallback((IMMNotificationClient*)this);        // if (hr==S_OK) cout<<"注册成功"<<endl;           // else cout<<"注册失败"<<endl;       }      ~CMMNotificationClient() {          SAFE_RELEASE(m_pEnumerator)        ::CoUninitialize();    }    // IUnknown methods -- AddRef, Release, and QueryInterface   private:      LONG _cRef;    JavaVM *vm;    jobject obj;    // Private function to print device-friendly name    HRESULT _PrintDeviceName(LPCWSTR pwstrId) {        HRESULT hr = S_OK;        IMMDevice *pDevice = NULL;        IPropertyStore *pProps = NULL;        PROPVARIANT varString;        CoInitialize(NULL);        PropVariantInit(&varString);        hr = m_pEnumerator -> GetDevice(pwstrId, &pDevice);        // if (hr != S_OK) ; // throw "Unable to get device"        hr = pDevice -> OpenPropertyStore(STGM_READ, &pProps);        // if (hr != S_OK) ; // throw "Unable to opt property store"        // Get the endpoint device's friendly-name property.        hr = pProps -> GetValue(PKEY_Device_FriendlyName, &varString);        // return varString.pwszVal        printf("%S\n", varString.pwszVal);        fflush(stdout);        JNIEnv *env;        vm -> AttachCurrentThread((void **) &env, NULL);        onEvent(env, obj, varString.pwszVal);        PropVariantClear(&varString);        SAFE_RELEASE(pProps)        SAFE_RELEASE(pDevice)        // return hr;    }    ULONG STDMETHODCALLTYPE AddRef() {          return InterlockedIncrement(&_cRef);      }      ULONG STDMETHODCALLTYPE Release() {          ULONG ulRef = InterlockedDecrement(&_cRef);          if (0 == ulRef) delete this;          return ulRef;      }      HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) {          if (IID_IUnknown == riid) {              AddRef();              *ppvInterface = (IUnknown*)this;          } else if (__uuidof(IMMNotificationClient) == riid) {              AddRef();              *ppvInterface = (IMMNotificationClient*)this;          } else {              *ppvInterface = NULL;              return E_NOINTERFACE;          }          return S_OK;      }      HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {return S_OK;}       HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) {           _PrintDeviceName(pwstrDeviceId);        return S_OK;       }   };jstring w2js(JNIEnv *env, LPWSTR src){    int src_len = wcslen(src);    jchar * dest = new jchar[src_len + 1];    memset(dest, 0, sizeof(jchar) * (src_len + 1));    for(int i = 0; i < src_len; i++)        memcpy(&dest[i], &src[i], 2);    jstring dst = env -> NewString(dest,src_len);    delete [] dest;    return dst;}void onEvent(JNIEnv *env, jobject obj, LPWSTR str) {    jstring jstr = w2js(env, str);    printf("jstr converted\n");    fflush(stdout);    jclass dpclazz = env -> FindClass("io/github/std4453/topdesk/headphone/HeadphonePeer");    printf("dpclazz %d\n", dpclazz);    fflush(stdout);    jmethodID method1 = env -> GetMethodID(dpclazz, "onEvent", "(Ljava/lang/String;)V");    printf("method1 %d\n", method1);    fflush(stdout);    env -> CallObjectMethod(obj,method1, jstr);}CMMNotificationClient *client;JNIEXPORT jstring JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStartListening(JNIEnv * env, jobject obj) {    JavaVM *vm;    env -> GetJavaVM(&vm);    obj = env -> NewGlobalRef(obj);    client = new CMMNotificationClient(vm, obj);    return NULL;}JNIEXPORT void JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStopListening(JNIEnv * env , jobject obj) {    delete client;}

编译运行:

输出

大功告成!

再删掉点调试信息,防止一下内存泄漏,加点儿异常处理:

#define SAFE_RELEASE(punk) \ if ((punk) != NULL) \ { (punk)->Release(); (punk) = NULL; }   #include <stdlib.h>#include <stdio.h>#include <windows.h>#include <setupapi.h>  #include <initguid.h>#include <mmdeviceapi.h>  #include <Functiondiscoverykeys_devpkey.h>#include "io_github_std4453_topdesk_headphone_HeadphonePeer.h"JavaVM *vm;jobject g_obj;jstring w2js(JNIEnv *env, LPCWSTR src) {    int src_len = wcslen(src);    jchar * dest = new jchar[src_len + 1];    memset(dest, 0, sizeof(jchar) * (src_len + 1));    for(int i = 0; i < src_len; i++)        memcpy(&dest[i], &src[i], 2);    jstring dst = env -> NewString(dest, src_len);    delete [] dest;    return dst;}void onEvent(LPCWSTR str) {    JNIEnv *env;    vm -> AttachCurrentThread((void **)&env, NULL);    jstring jstr = w2js(env, str);    jclass dpclazz = env -> FindClass("io/github/std4453/topdesk/headphone/HeadphonePeer");    jmethodID method1 = env -> GetMethodID(dpclazz, "onEvent", "(Ljava/lang/String;)V");    env -> CallObjectMethod(g_obj, method1, jstr);    env -> DeleteLocalRef(jstr);    env -> DeleteLocalRef(dpclazz);    vm -> DetachCurrentThread();}class CMMNotificationClient: public IMMNotificationClient {   public:      IMMDeviceEnumerator *m_pEnumerator;      CMMNotificationClient(): _cRef(1), started(false), m_pEnumerator(NULL) {}    ~CMMNotificationClient() {          SAFE_RELEASE(m_pEnumerator);        if (started) CoUninitialize();    }    LPCWSTR startListener() {        HRESULT hr = S_OK;        CoInitialize(NULL);          hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&m_pEnumerator);           if (hr != S_OK) return L"Unable to create interface!";        hr = m_pEnumerator->RegisterEndpointNotificationCallback((IMMNotificationClient*)this);        if (hr != S_OK) return L"Unable to register listener!";        started = true;        return NULL;    }private:      LONG _cRef;    boolean started;    ULONG STDMETHODCALLTYPE AddRef() {          return InterlockedIncrement(&_cRef);      }      ULONG STDMETHODCALLTYPE Release() {          ULONG ulRef = InterlockedDecrement(&_cRef);          if (0 == ulRef) delete this;          return ulRef;      }      HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) {          if (IID_IUnknown == riid) {              AddRef();              *ppvInterface = (IUnknown*)this;          } else if (__uuidof(IMMNotificationClient) == riid) {              AddRef();              *ppvInterface = (IMMNotificationClient*)this;          } else {              *ppvInterface = NULL;              return E_NOINTERFACE;          }          return S_OK;      }      HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {return S_OK;}       HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) {return S_OK;}    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) {           HRESULT hr = S_OK;        IMMDevice *pDevice = NULL;        IPropertyStore *pProps = NULL;        PROPVARIANT varString;        CoInitialize(NULL);        PropVariantInit(&varString);        hr = m_pEnumerator -> GetDevice(pwstrDeviceId, &pDevice);        if (hr == S_OK) hr = pDevice -> OpenPropertyStore(STGM_READ, &pProps);        if (hr == S_OK) hr = pProps -> GetValue(PKEY_Device_FriendlyName, &varString);        if (hr == S_OK) onEvent(varString.pwszVal);        PropVariantClear(&varString);        SAFE_RELEASE(pProps);        SAFE_RELEASE(pDevice);        CoUninitialize();           return S_OK;       }   };CMMNotificationClient *client;JNIEXPORT jstring JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStartListening(JNIEnv * env, jobject obj) {    env -> GetJavaVM(&vm);    g_obj = env -> NewGlobalRef(obj);    if (g_obj == NULL) return w2js(env, L"Cannot create global reference to obj!");    client = new CMMNotificationClient();    LPCWSTR msg = client -> startListener();    if (msg == NULL) return NULL;    else return w2js(env, msg);}JNIEXPORT void JNICALL Java_io_github_std4453_topdesk_headphone_HeadphonePeer_nStopListening(JNIEnv * env , jobject obj) {    delete client;}
package io.github.std4453.topdesk.headphone;import java.io.BufferedReader;import java.io.InputStreamReader;/** * */public class HeadphoneTest {    public static void main(String[] args) throws Exception {        System.loadLibrary("topdesk");        HeadphonePeer peer = new HeadphonePeer();        peer.addListener(new HeadphoneEventListener() {            @Override            public void onHeadphoneInserted() {                System.out.println("Headphone inserted!");            }            @Override            public void onHeadphoneRemoved() {                System.out.println("Headphone removed!");            }        });        System.out.println("Press enter to exit.");        new BufferedReader(new InputStreamReader(System.in)).readLine();        System.exit(0);    }}
package io.github.std4453.topdesk.headphone;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * */public class HeadphonePeer {    private List<HeadphoneEventListener> listeners ;    private boolean inserted ;    private ExecutorService executor;    public HeadphonePeer() {        this.inserted = false;        this.listeners = new ArrayList<>();        this.executor = Executors.newSingleThreadExecutor();        this.startListening();        Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));    }    public void addListener(HeadphoneEventListener listener) {        this.listeners.add(listener);    }    public void onEvent(String name) {        if (name.startsWith("External")) this.onInsert();        else if (name.startsWith("Internal")) this.onRemove();    }    private synchronized void onInsert() {        if (!this.inserted) {            this.inserted = true;            this.executor.submit(() -> this.listeners.forEach                    (HeadphoneEventListener::onHeadphoneInserted));        }    }    private synchronized void onRemove() {        if (this.inserted) {            this.inserted = false;            this.executor.submit(() -> this.listeners.forEach                    (HeadphoneEventListener::onHeadphoneRemoved));        }    }    private void startListening() {        String msg = nStartListening();        if (msg != null) throw new RuntimeException(msg);    }    private void shutdown() {        this.executor.shutdown();        this.nStopListening();    }    private native String nStartListening();    private native void nStopListening();}

运行一下:

运行结果

最后美中不足的还有两点:一是这种方法无法检测最初的时候默认耳机拔出,也就是说,如果启动时耳机就处于插入状态,则第一次拔出时不会有提示。二是拔出时大约有2秒左右的延时,一开始几个扬声器的OnPropertyValueChanged事件会一个一个跳出来,速度很慢,倒是插入时反应相当快。

0x05 我们处于社会主义初级阶段

至此耳机插拔的功能已经基本完成了,从底层的c++到JNI到Java接口,接下来与此相关的前端以及插件开发,肯定要等到插件框架开发完了,而且也不会放在底层线里面了。

接下来实现什么呢?会是继续写win32来检测u盘插拔吗?又或者是写安卓+蓝牙来做手机的功能呢?这样想来,倒是还有点兴奋。

越是折腾,越是爱折腾——这大概就是一个程序员的自我修养吧。

最后来个小彩蛋。
前面搜CoInitialize和CoUninitialize的调用问题,找到了这个页面,结果一看讨论的人:

轮子哥出没请注意

本来嫌知乎编辑器太烂才来的这边,结果随随便便都能碰得到轮子哥,还当真是踏破铁鞋无觅处,得来全不费工夫。

人生之妙,莫过于此。

原创粉丝点击