605 lines
17 KiB
TypeScript
605 lines
17 KiB
TypeScript
/**
|
||
* WiFi 工具类 (Android App 专用)
|
||
* 用于 uni-app Android 平台 WiFi 操作
|
||
*
|
||
* ======================== 使用说明 ========================
|
||
*
|
||
* import wifi from '@/utils/wifi'
|
||
*
|
||
* // 检查 WiFi 是否可用
|
||
* const available = await wifi.isWifiAvailable()
|
||
*
|
||
* // 获取当前连接信息
|
||
* const info = await wifi.getConnectedWifi()
|
||
*
|
||
* // 扫描附近 WiFi
|
||
* const networks = await wifi.scanWifi()
|
||
*
|
||
* // 连接指定 WiFi
|
||
* await wifi.connectWifi({ SSID: 'MyWiFi', password: '12345678' })
|
||
*
|
||
* // 断开当前连接
|
||
* await wifi.disconnectWifi()
|
||
*
|
||
* // 连接已保存的 WiFi(免密)
|
||
* await wifi.connectSavedWifi({ SSID: 'MyWiFi' })
|
||
*
|
||
* // 获取已保存的 WiFi 列表
|
||
* const savedList = await wifi.getSavedWifiList()
|
||
*
|
||
* // 移除已保存的 WiFi
|
||
* await wifi.removeSavedWifi({ SSID: 'MyWiFi' })
|
||
*
|
||
* // 监听 WiFi 状态变化
|
||
* const unwatch = wifi.onWifiStateChanged((state) => {
|
||
* console.log(state.isConnected ? '已连接' : '未连接')
|
||
* })
|
||
* unwatch() // 取消监听
|
||
*
|
||
* // 监听扫描结果
|
||
* const unwatch = wifi.onWifiScan((networks) => {
|
||
* console.log('扫描到', networks.length, '个网络')
|
||
* })
|
||
* unwatch() // 取消监听
|
||
*
|
||
* // 获取信号强度描述
|
||
* wifi.getSignalDescription(-55) // "强"
|
||
*
|
||
* // 判断频段
|
||
* wifi.is5GHz(5180) // true
|
||
*
|
||
* ======================== 前置条件 ========================
|
||
*
|
||
* - 仅支持 Android App 环境
|
||
* - 需要在 manifest.json 配置权限:
|
||
* - android.permission.ACCESS_WIFI_STATE
|
||
* - android.permission.CHANGE_WIFI_STATE
|
||
* - android.permission.ACCESS_NETWORK_STATE
|
||
* - android.permission.ACCESS_FINE_LOCATION
|
||
* - android.permission.ACCESS_COARSE_LOCATION
|
||
* - Android 6+ 还需运行时授予定位权限,且系统「位置信息」开关需开启
|
||
*/
|
||
|
||
// ======================== 类型定义 ========================
|
||
|
||
/** WiFi 网络信息 */
|
||
export interface WifiInfo {
|
||
SSID: string
|
||
BSSID: string
|
||
signalStrength: number
|
||
securityType?: string
|
||
frequency?: number
|
||
connected?: boolean
|
||
}
|
||
|
||
/** 连接 WiFi 参数 */
|
||
export interface ConnectWifiParams {
|
||
SSID: string
|
||
password?: string
|
||
timeout?: number
|
||
}
|
||
|
||
/** WiFi 状态变化回调 */
|
||
export type WifiStateCallback = (state: {
|
||
isConnected: boolean
|
||
wifiInfo: WifiInfo | null
|
||
}) => void
|
||
|
||
/** WiFi 扫描结果回调 */
|
||
export type WifiScanCallback = (networks: WifiInfo[]) => void
|
||
|
||
// ======================== 内部工具 ========================
|
||
|
||
const listeners = {
|
||
stateChange: new Map<string, WifiStateCallback>(),
|
||
scan: new Map<string, WifiScanCallback>(),
|
||
}
|
||
|
||
let receiverId = 0
|
||
|
||
/** 获取 WiFi Manager 实例 */
|
||
function getWifiManager() {
|
||
const ctx = (plus.android as any).importClass('android.content.Context')
|
||
const activity = (plus.android as any).runtimeMainActivity()
|
||
return activity.getSystemService(ctx.WIFI_SERVICE)
|
||
}
|
||
|
||
/** 调用 Android 方法 */
|
||
function invoke(obj: any, method: string, ...args: any[]) {
|
||
return (plus.android as any).invoke(obj, method, ...args)
|
||
}
|
||
|
||
/** 导入 Android 类 */
|
||
function importClass(name: string) {
|
||
return (plus.android as any).importClass(name)
|
||
}
|
||
|
||
/** 读取 Java 对象字段(ScanResult.SSID 等是字段不是方法) */
|
||
function getAttr(obj: any, name: string) {
|
||
try {
|
||
return (plus.android as any).getAttribute(obj, name)
|
||
}
|
||
catch {
|
||
return invoke(obj, name)
|
||
}
|
||
}
|
||
|
||
/** 申请定位权限(Android 扫描 WiFi 必需) */
|
||
function requestLocationPermission(): Promise<boolean> {
|
||
return new Promise((resolve) => {
|
||
try {
|
||
;(plus.android as any).requestPermissions(
|
||
[
|
||
'android.permission.ACCESS_FINE_LOCATION',
|
||
'android.permission.ACCESS_COARSE_LOCATION',
|
||
],
|
||
(result: { granted?: string[], deniedAlways?: string[], deniedPresent?: string[] }) => {
|
||
const granted = result?.granted || []
|
||
const ok = granted.includes('android.permission.ACCESS_FINE_LOCATION')
|
||
|| granted.includes('android.permission.ACCESS_COARSE_LOCATION')
|
||
console.log('定位权限申请结果:', result)
|
||
resolve(ok)
|
||
},
|
||
(err: unknown) => {
|
||
console.error('定位权限申请失败:', err)
|
||
resolve(false)
|
||
},
|
||
)
|
||
}
|
||
catch (err) {
|
||
console.error('requestPermissions 不可用:', err)
|
||
resolve(false)
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 系统定位开关是否开启 */
|
||
function isLocationEnabled(): boolean {
|
||
try {
|
||
const LocationManager = importClass('android.location.LocationManager')
|
||
const Context = importClass('android.content.Context')
|
||
const activity = (plus.android as any).runtimeMainActivity()
|
||
const lm = activity.getSystemService(Context.LOCATION_SERVICE)
|
||
return !!(
|
||
invoke(lm, 'isProviderEnabled', LocationManager.GPS_PROVIDER)
|
||
|| invoke(lm, 'isProviderEnabled', LocationManager.NETWORK_PROVIDER)
|
||
)
|
||
}
|
||
catch {
|
||
return true
|
||
}
|
||
}
|
||
|
||
/** 解析 getScanResults 列表 */
|
||
function parseScanResults(wm: any): WifiInfo[] {
|
||
const results = invoke(wm, 'getScanResults')
|
||
if (!results)
|
||
return []
|
||
|
||
const size = invoke(results, 'size') || 0
|
||
const networks: WifiInfo[] = []
|
||
|
||
for (let i = 0; i < size; i++) {
|
||
const r = invoke(results, 'get', i)
|
||
const ssid = String(getAttr(r, 'SSID') || '').replace(/"/g, '')
|
||
if (!ssid || ssid === '<unknown ssid>')
|
||
continue
|
||
|
||
networks.push({
|
||
SSID: ssid,
|
||
BSSID: String(getAttr(r, 'BSSID') || ''),
|
||
signalStrength: Number(getAttr(r, 'level') || 0),
|
||
securityType: String(getAttr(r, 'capabilities') || ''),
|
||
frequency: Number(getAttr(r, 'frequency') || 0),
|
||
connected: false,
|
||
})
|
||
}
|
||
|
||
return networks
|
||
}
|
||
|
||
// ======================== WiFi 工具 ========================
|
||
|
||
const wifi = {
|
||
/** 检查 WiFi 是否可用 */
|
||
isWifiAvailable(): Promise<boolean> {
|
||
return new Promise((resolve) => {
|
||
try {
|
||
const wm = getWifiManager()
|
||
resolve(!!invoke(wm, 'isWifiEnabled'))
|
||
}
|
||
catch {
|
||
resolve(false)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 获取当前 WiFi 连接信息 */
|
||
getConnectedWifi(): Promise<WifiInfo | null> {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const wm = getWifiManager()
|
||
const info = invoke(wm, 'getConnectionInfo')
|
||
const ssid = invoke(info, 'getSSID')
|
||
const bssid = invoke(info, 'getBSSID')
|
||
const rssi = invoke(info, 'getRssi')
|
||
const networkId = invoke(info, 'getNetworkId')
|
||
|
||
if (!ssid || ssid === '<unknown ssid>' || networkId === -1) {
|
||
resolve(null)
|
||
return
|
||
}
|
||
|
||
resolve({
|
||
SSID: ssid.replace(/"/g, ''),
|
||
BSSID: bssid || '',
|
||
signalStrength: rssi,
|
||
connected: true,
|
||
})
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 扫描附近 WiFi 网络 */
|
||
async scanWifi(): Promise<WifiInfo[]> {
|
||
const wm = getWifiManager()
|
||
const isEnabled = invoke(wm, 'isWifiEnabled')
|
||
console.log('WiFi 是否开启:', isEnabled)
|
||
|
||
if (!isEnabled)
|
||
throw new Error('请先开启 WiFi')
|
||
|
||
const hasPermission = await requestLocationPermission()
|
||
if (!hasPermission)
|
||
throw new Error('需要定位权限才能扫描 WiFi,请在系统设置中允许')
|
||
|
||
if (!isLocationEnabled())
|
||
throw new Error('请先打开系统「位置信息/定位」开关,否则无法扫描 WiFi')
|
||
|
||
return new Promise((resolve, reject) => {
|
||
let settled = false
|
||
let receiver: any = null
|
||
let timer: ReturnType<typeof setTimeout> | undefined
|
||
const activity = (plus.android as any).runtimeMainActivity()
|
||
|
||
const finish = (networks: WifiInfo[], err?: Error) => {
|
||
if (settled)
|
||
return
|
||
settled = true
|
||
if (timer)
|
||
clearTimeout(timer)
|
||
try {
|
||
if (receiver)
|
||
activity.unregisterReceiver(receiver)
|
||
}
|
||
catch {}
|
||
if (err)
|
||
reject(err)
|
||
else
|
||
resolve(networks)
|
||
}
|
||
|
||
try {
|
||
const Filter = importClass('android.content.IntentFilter')
|
||
const Receiver = importClass('android.content.BroadcastReceiver')
|
||
const filter = new Filter()
|
||
filter.addAction('android.net.wifi.SCAN_RESULTS')
|
||
|
||
receiver = new Receiver({
|
||
onReceive(_ctx: any, intent: any) {
|
||
const updated = invoke(intent, 'getBooleanExtra', 'resultsUpdated', false)
|
||
console.log('收到 SCAN_RESULTS, resultsUpdated:', updated)
|
||
const networks = parseScanResults(wm)
|
||
console.log('解析后网络数量:', networks.length)
|
||
finish(networks)
|
||
},
|
||
})
|
||
activity.registerReceiver(receiver, filter)
|
||
|
||
const started = invoke(wm, 'startScan')
|
||
console.log('WiFi 扫描启动:', started)
|
||
|
||
// startScan 失败时仍尝试读取缓存结果(限流时常见)
|
||
if (!started) {
|
||
const cached = parseScanResults(wm)
|
||
console.log('startScan 失败,缓存网络数量:', cached.length)
|
||
if (cached.length > 0) {
|
||
finish(cached)
|
||
return
|
||
}
|
||
finish([], new Error('WiFi 扫描被拒绝(可能被系统限流,请稍后再试)'))
|
||
return
|
||
}
|
||
}
|
||
catch (err) {
|
||
finish([], err instanceof Error ? err : new Error(String(err)))
|
||
return
|
||
}
|
||
|
||
timer = setTimeout(() => {
|
||
const networks = parseScanResults(wm)
|
||
console.log('扫描超时,当前网络数量:', networks.length)
|
||
if (networks.length > 0)
|
||
finish(networks)
|
||
else
|
||
finish([], new Error('扫描超时,未发现可用网络'))
|
||
}, 8000)
|
||
})
|
||
},
|
||
|
||
/** 连接指定 WiFi */
|
||
connectWifi(params: ConnectWifiParams): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const { SSID, password, timeout = 10000 } = params
|
||
if (!SSID) {
|
||
reject(new Error('SSID 不能为空'))
|
||
return
|
||
}
|
||
|
||
try {
|
||
const wm = getWifiManager()
|
||
const Config = importClass('android.net.wifi.WifiConfiguration')
|
||
const config = new Config()
|
||
|
||
invoke(config, 'SSID', `"${SSID}"`)
|
||
config.networkId = -1
|
||
|
||
const keyMgmt = invoke(config, 'allowedKeyManagement')
|
||
if (password) {
|
||
config.preSharedKey = `"${password}"`
|
||
invoke(keyMgmt, 'set', 1) // WPA_PSK
|
||
}
|
||
else {
|
||
invoke(keyMgmt, 'set', 0) // NONE
|
||
}
|
||
|
||
const networkId = invoke(wm, 'addNetwork', config)
|
||
|
||
if (networkId === -1) {
|
||
const networks = invoke(wm, 'getConfiguredNetworks')
|
||
const size = invoke(networks, 'size')
|
||
for (let i = 0; i < size; i++) {
|
||
const existing = invoke(networks, 'get', i)
|
||
if (invoke(existing, 'SSID') === `"${SSID}"`) {
|
||
invoke(wm, 'updateNetwork', existing, config)
|
||
invoke(wm, 'disconnect')
|
||
invoke(wm, 'enableNetwork', invoke(existing, 'networkId'), true)
|
||
invoke(wm, 'reconnect')
|
||
resolve()
|
||
return
|
||
}
|
||
}
|
||
reject(new Error('无法连接到网络'))
|
||
return
|
||
}
|
||
|
||
invoke(wm, 'disconnect')
|
||
invoke(wm, 'enableNetwork', networkId, true)
|
||
invoke(wm, 'reconnect')
|
||
|
||
let elapsed = 0
|
||
const timer = setInterval(() => {
|
||
const info = invoke(wm, 'getConnectionInfo')
|
||
if (invoke(info, 'getSupplicantState') === 3) {
|
||
clearInterval(timer)
|
||
resolve()
|
||
return
|
||
}
|
||
elapsed += 500
|
||
if (elapsed >= timeout) {
|
||
clearInterval(timer)
|
||
reject(new Error('连接超时'))
|
||
}
|
||
}, 500)
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 断开当前 WiFi 连接 */
|
||
disconnectWifi(): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const wm = getWifiManager()
|
||
invoke(wm, 'disableNetwork', -1)
|
||
const result = invoke(wm, 'disconnect')
|
||
result ? resolve() : reject(new Error('断开连接失败'))
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 连接已保存的 WiFi(免密) */
|
||
connectSavedWifi(params: { SSID: string }): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const { SSID } = params
|
||
try {
|
||
const wm = getWifiManager()
|
||
const networks = invoke(wm, 'getConfiguredNetworks')
|
||
const size = invoke(networks, 'size')
|
||
|
||
for (let i = 0; i < size; i++) {
|
||
const config = invoke(networks, 'get', i)
|
||
if (invoke(config, 'SSID') === `"${SSID}"`) {
|
||
invoke(wm, 'disconnect')
|
||
invoke(wm, 'enableNetwork', invoke(config, 'networkId'), true)
|
||
invoke(wm, 'reconnect')
|
||
resolve()
|
||
return
|
||
}
|
||
}
|
||
|
||
reject(new Error(`未找到已保存的网络: ${SSID}`))
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 获取已保存的 WiFi 列表 */
|
||
getSavedWifiList(): Promise<WifiInfo[]> {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const wm = getWifiManager()
|
||
const networks = invoke(wm, 'getConfiguredNetworks')
|
||
const size = invoke(networks, 'size')
|
||
const result: WifiInfo[] = []
|
||
|
||
for (let i = 0; i < size; i++) {
|
||
const config = invoke(networks, 'get', i)
|
||
const ssid = invoke(config, 'SSID').replace(/"/g, '')
|
||
if (!ssid)
|
||
continue
|
||
|
||
result.push({
|
||
SSID: ssid,
|
||
BSSID: invoke(config, 'BSSID') || '',
|
||
signalStrength: 0,
|
||
connected: false,
|
||
})
|
||
}
|
||
|
||
resolve(result)
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 移除已保存的 WiFi 网络 */
|
||
removeSavedWifi(params: { SSID: string }): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const { SSID } = params
|
||
try {
|
||
const wm = getWifiManager()
|
||
const networks = invoke(wm, 'getConfiguredNetworks')
|
||
const size = invoke(networks, 'size')
|
||
|
||
for (let i = 0; i < size; i++) {
|
||
const config = invoke(networks, 'get', i)
|
||
if (invoke(config, 'SSID') === `"${SSID}"`) {
|
||
invoke(wm, 'removeNetwork', invoke(config, 'networkId'))
|
||
invoke(wm, 'saveConfiguration')
|
||
resolve()
|
||
return
|
||
}
|
||
}
|
||
|
||
reject(new Error(`未找到已保存的网络: ${SSID}`))
|
||
}
|
||
catch (err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 监听 WiFi 状态变化 */
|
||
onWifiStateChanged(callback: WifiStateCallback): () => void {
|
||
const id = `state_${++receiverId}`
|
||
listeners.stateChange.set(id, callback)
|
||
|
||
try {
|
||
const activity = (plus.android as any).runtimeMainActivity()
|
||
const Filter = importClass('android.content.IntentFilter')
|
||
const Receiver = importClass('android.content.BroadcastReceiver')
|
||
|
||
const filter = new Filter()
|
||
filter.addAction('android.net.wifi.STATE_CHANGE')
|
||
|
||
const receiver = new Receiver({
|
||
onReceive(_ctx: any, intent: any) {
|
||
const action = invoke(intent, 'getAction')
|
||
if (action !== 'android.net.wifi.STATE_CHANGE')
|
||
return
|
||
|
||
const networkInfo = invoke(intent, 'getParcelableExtra', 'networkInfo')
|
||
const state = invoke(networkInfo, 'getState')
|
||
|
||
if (state === 2) {
|
||
wifi.getConnectedWifi().then(info => callback({ isConnected: true, wifiInfo: info }))
|
||
}
|
||
else {
|
||
callback({ isConnected: false, wifiInfo: null })
|
||
}
|
||
},
|
||
})
|
||
|
||
activity.registerReceiver(receiver, filter)
|
||
}
|
||
catch {}
|
||
|
||
return () => listeners.stateChange.delete(id)
|
||
},
|
||
|
||
/** 监听 WiFi 扫描结果 */
|
||
onWifiScan(callback: WifiScanCallback): () => void {
|
||
const id = `scan_${++receiverId}`
|
||
listeners.scan.set(id, callback)
|
||
|
||
try {
|
||
const activity = (plus.android as any).runtimeMainActivity()
|
||
const Filter = importClass('android.content.IntentFilter')
|
||
const Receiver = importClass('android.content.BroadcastReceiver')
|
||
|
||
const filter = new Filter()
|
||
filter.addAction('android.net.wifi.SCAN_RESULTS')
|
||
|
||
const receiver = new Receiver({
|
||
onReceive() {
|
||
const wm = getWifiManager()
|
||
callback(parseScanResults(wm))
|
||
},
|
||
})
|
||
|
||
activity.registerReceiver(receiver, filter)
|
||
}
|
||
catch {}
|
||
|
||
return () => listeners.scan.delete(id)
|
||
},
|
||
|
||
/** 获取信号强度等级 (0-4) */
|
||
getSignalLevel(rssi: number): number {
|
||
if (rssi >= -50)
|
||
return 4
|
||
if (rssi >= -60)
|
||
return 3
|
||
if (rssi >= -70)
|
||
return 2
|
||
if (rssi >= -80)
|
||
return 1
|
||
return 0
|
||
},
|
||
|
||
/** 获取信号强度描述 */
|
||
getSignalDescription(rssi: number): string {
|
||
const level = this.getSignalLevel(rssi)
|
||
return ['无信号', '弱', '一般', '强', '非常强'][level] || '未知'
|
||
},
|
||
|
||
/** 判断是否为 5GHz 频段 */
|
||
is5GHz(frequency: number): boolean {
|
||
return frequency >= 5000 && frequency <= 5900
|
||
},
|
||
|
||
/** 获取频段描述 */
|
||
getFrequencyDescription(frequency: number): string {
|
||
if (this.is5GHz(frequency))
|
||
return '5GHz'
|
||
if (frequency >= 2400 && frequency <= 2500)
|
||
return '2.4GHz'
|
||
return '未知'
|
||
},
|
||
}
|
||
|
||
export default wifi
|