From 9dc3e2ddf73b67c488f33eeedbc1826dd4155247 Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Thu, 25 Jun 2026 19:59:04 +0800 Subject: [PATCH] =?UTF-8?q?M3(=E6=89=B94):=20=E5=9B=BE=E7=89=87/=E6=91=87?= =?UTF-8?q?=E4=B8=80=E6=91=87/=E6=89=AB=E7=A0=81/=E7=9B=B8=E6=9C=BA/?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=20Provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T-M3-07 PhotoProvider:getphoto 批量下载到资源根 photos/(file:// 白名单内) → 出站 getphoto[{pid,photourl}],复用 Downloader 并行 T-M3-08 ShakeProvider:startshake/SwitchShake/stopshake,@ohos.sensor 加速度合力阈值检测+节流,~1s 后出站 shakeEnd(空串),反馈震动 T-M3-11 ScanProvider:opensaoma → @kit.ScanKit 默认界面扫码(免权限) → 出站 getsaomaData(originalValue) T-M3-12 CameraProvider:opencamera → @kit.CameraKit cameraPicker.pick(免权限) → 出站 getcameraaAddress(resultUri) T-M3-09 LocationProvider:startlocation(连续/单次,用时申请权限)/getlocationinfo(同步) → geoLocationManager 定位+逆地理编码 → 出站 getlocationinfo(MaplocationInfo;错误码 0/-1未就绪/-2权限拒绝) module.json5 增 ACCELEROMETER/LOCATION/APPROXIMATELY_LOCATION;组装根装配 5 者 devecocli build 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) --- entry/src/main/ets/di/AppModule.ets | 8 +- entry/src/main/module.json5 | 19 ++ .../main/resources/base/element/string.json | 4 + feature_capabilities/Index.ets | 5 + .../src/main/ets/providers/CameraProvider.ets | 48 +++++ .../main/ets/providers/LocationProvider.ets | 171 ++++++++++++++++++ .../src/main/ets/providers/PhotoProvider.ets | 71 ++++++++ .../src/main/ets/providers/ScanProvider.ets | 42 +++++ .../src/main/ets/providers/ShakeProvider.ets | 96 ++++++++++ 9 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 feature_capabilities/src/main/ets/providers/CameraProvider.ets create mode 100644 feature_capabilities/src/main/ets/providers/LocationProvider.ets create mode 100644 feature_capabilities/src/main/ets/providers/PhotoProvider.ets create mode 100644 feature_capabilities/src/main/ets/providers/ScanProvider.ets create mode 100644 feature_capabilities/src/main/ets/providers/ShakeProvider.ets diff --git a/entry/src/main/ets/di/AppModule.ets b/entry/src/main/ets/di/AppModule.ets index a430013..0bb1b07 100644 --- a/entry/src/main/ets/di/AppModule.ets +++ b/entry/src/main/ets/di/AppModule.ets @@ -7,7 +7,8 @@ */ import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider, VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider, - NetworkProvider, ShareProvider, LoginProvider } from 'feature_capabilities'; + NetworkProvider, ShareProvider, LoginProvider, PhotoProvider, ShakeProvider, + ScanProvider, CameraProvider, LocationProvider } from 'feature_capabilities'; export function buildCapabilities(): CapabilityProvider[] { return [ @@ -20,6 +21,11 @@ export function buildCapabilities(): CapabilityProvider[] { new NetworkProvider(), // getnetwork + 出站广播 new ShareProvider(), // friendsSharetypeUrlToptitleDescript → sharesuccess(微信) new LoginProvider(), // accreditlogin → sharelogin(微信 code 模式,profile 待服务端) + new PhotoProvider(), // getphoto 批量下载 → getphoto + new ShakeProvider(), // startshake/SwitchShake/stopshake → shakeEnd + new ScanProvider(), // opensaoma → getsaomaData + new CameraProvider(), // opencamera → getcameraaAddress + new LocationProvider(), // startlocation/getlocationinfo → getlocationinfo // —— 暂缓能力占位桩(§6.5)—— new RoomStubProvider(), new PayStubProvider(), diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index 9adba86..4f03ed5 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -67,6 +67,25 @@ }, { "name": "ohos.permission.GET_WIFI_INFO" + }, + { + "name": "ohos.permission.ACCELEROMETER" + }, + { + "name": "ohos.permission.LOCATION", + "reason": "$string:reason_location", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:reason_location", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } } ] } diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index a2017d8..7dca805 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -15,6 +15,10 @@ { "name": "reason_internet", "value": "用于下载游戏资源与获取远程配置" + }, + { + "name": "reason_location", + "value": "用于向游戏提供定位服务" } ] } \ No newline at end of file diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index fb80e25..3f6947a 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -16,6 +16,11 @@ export { DeviceProvider } from './src/main/ets/providers/DeviceProvider'; export { NetworkProvider } from './src/main/ets/providers/NetworkProvider'; export { ShareProvider } from './src/main/ets/providers/ShareProvider'; export { LoginProvider } from './src/main/ets/providers/LoginProvider'; +export { PhotoProvider } from './src/main/ets/providers/PhotoProvider'; +export { ShakeProvider } from './src/main/ets/providers/ShakeProvider'; +export { ScanProvider } from './src/main/ets/providers/ScanProvider'; +export { CameraProvider } from './src/main/ets/providers/CameraProvider'; +export { LocationProvider } from './src/main/ets/providers/LocationProvider'; // 微信 SDK 封装(EntryAbility 处理回调 want 需用) export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi'; diff --git a/feature_capabilities/src/main/ets/providers/CameraProvider.ets b/feature_capabilities/src/main/ets/providers/CameraProvider.ets new file mode 100644 index 0000000..1bf1872 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/CameraProvider.ets @@ -0,0 +1,48 @@ +/** + * 相机拍照能力(契约 §8.8/§9,T-M3-12)。@kit.CameraKit CameraPicker。 + * - opencamera:拉起系统相机拍照 → 出站 getcameraaAddress(拼写原样双 a,照片 uri 裸串)。 + * 系统相机选择器无需申请相机权限。 + */ +import { camera, cameraPicker } from '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { common } from '@kit.AbilityKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +export class CameraProvider implements CapabilityProvider { + readonly name: string = 'camera'; + private readonly log: Logger = Logger.tag('CameraProvider'); + private bridge: BridgeController | undefined = undefined; + private context: common.UIAbilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.context = ctx.uiAbilityContext; + bridge.registerHandler(InboundHandlers.OpenCamera, (_d: string, _cb: (resp: string) => void) => { + this.capture(); + }); + } + + private capture(): void { + const ctx = this.context; + if (ctx === undefined) { + return; + } + const profile: cameraPicker.PickerProfile = { + cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK, + }; + cameraPicker.pick(ctx, [cameraPicker.PickerMediaType.PHOTO], profile) + .then((result: cameraPicker.PickerResult) => { + if (result.resultCode === 0 && result.resultUri !== '') { + this.bridge?.callHandler(OutboundHandlers.GetCameraAddress, result.resultUri); + } else { + this.log.i(`camera canceled: resultCode=${result.resultCode}`); + } + }) + .catch((e: BusinessError) => { + this.log.i(`camera failed: ${e.code} ${e.message}`); + }); + } +} diff --git a/feature_capabilities/src/main/ets/providers/LocationProvider.ets b/feature_capabilities/src/main/ets/providers/LocationProvider.ets new file mode 100644 index 0000000..eefef64 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/LocationProvider.ets @@ -0,0 +1,171 @@ +/** + * 定位能力(契约 §8.6/§9/§10.8,T-M3-09)。@kit.LocationKit geoLocationManager。 + * - startlocation:data="1" 连续 / 其他 单次。用时申请定位权限;定位成功逆地理编码后出站 getlocationinfo。 + * - getlocationinfo:同步返回最近一次 MaplocationInfo(未就绪 errorCode=-1 / 权限拒绝 -2)。 + * 需 module.json5 声明 LOCATION + APPROXIMATELY_LOCATION,运行时申请。 + */ +import { geoLocationManager } from '@kit.LocationKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { common, Permissions } from '@kit.AbilityKit'; +import { PermissionGuard } from 'platform'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers, MaplocationInfo } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +const LOC_PERMS: Permissions[] = ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']; + +export class LocationProvider implements CapabilityProvider { + readonly name: string = 'location'; + private readonly log: Logger = Logger.tag('LocationProvider'); + private bridge: BridgeController | undefined = undefined; + private context: common.UIAbilityContext | undefined = undefined; + private lastInfo: MaplocationInfo | undefined = undefined; + private continuousOn: boolean = false; + private readonly onLocation: (loc: geoLocationManager.Location) => void; + + constructor() { + this.onLocation = (loc: geoLocationManager.Location) => this.handleLocation(loc); + } + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.context = ctx.uiAbilityContext; + bridge.registerHandler(InboundHandlers.StartLocation, (data: string, _cb: (resp: string) => void) => { + this.start(data === '1'); + }); + bridge.registerHandler(InboundHandlers.GetLocationInfo, (_d: string, cb: (resp: string) => void) => { + cb(JSON.stringify(this.lastInfo ?? this.errorInfo(-1))); + }); + } + + onDestroy(): void { + this.stopContinuous(); + } + + private async start(continuous: boolean): Promise { + const ctx = this.context; + if (ctx === undefined) { + return; + } + const granted: boolean = await PermissionGuard.request(ctx, LOC_PERMS); + if (!granted) { + this.lastInfo = this.errorInfo(-2); + this.push(this.lastInfo); + return; + } + if (continuous) { + this.startContinuous(); + } else { + this.requestSingle(); + } + } + + private requestSingle(): void { + const request: geoLocationManager.CurrentLocationRequest = { + priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, + scenario: geoLocationManager.LocationRequestScenario.UNSET, + }; + geoLocationManager.getCurrentLocation(request) + .then((loc: geoLocationManager.Location) => this.handleLocation(loc)) + .catch((e: BusinessError) => { + this.log.w(`getCurrentLocation failed: ${e.code} ${e.message}`); + this.lastInfo = this.errorInfo(e.code); + this.push(this.lastInfo); + }); + } + + private startContinuous(): void { + if (this.continuousOn) { + return; + } + const request: geoLocationManager.LocationRequest = { + priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, + scenario: geoLocationManager.LocationRequestScenario.UNSET, + timeInterval: 5, + distanceInterval: 0, + maxAccuracy: 0, + }; + try { + geoLocationManager.on('locationChange', request, this.onLocation); + this.continuousOn = true; + } catch (e) { + this.log.w(`on locationChange failed: ${(e as BusinessError).message}`); + } + } + + private stopContinuous(): void { + if (!this.continuousOn) { + return; + } + try { + geoLocationManager.off('locationChange', this.onLocation); + } catch (e) { + this.log.w(`off locationChange failed: ${(e as BusinessError).message}`); + } + this.continuousOn = false; + } + + /** 定位成功 → 逆地理编码(best-effort)→ 构建 MaplocationInfo → 出站。 */ + private handleLocation(loc: geoLocationManager.Location): void { + const base: MaplocationInfo = this.baseInfo(loc); + const req: geoLocationManager.ReverseGeoCodeRequest = { + latitude: loc.latitude, + longitude: loc.longitude, + maxItems: 1, + }; + geoLocationManager.getAddressesFromLocation(req) + .then((addrs: geoLocationManager.GeoAddress[]) => { + if (addrs.length > 0) { + this.fillAddress(base, addrs[0]); + } + this.lastInfo = base; + this.push(base); + }) + .catch((e: BusinessError) => { + this.log.i(`reverse geocode failed (${e.code}); push coords only`); + this.lastInfo = base; + this.push(base); + }); + } + + private baseInfo(loc: geoLocationManager.Location): MaplocationInfo { + return { + latitude: loc.latitude, + longitude: loc.longitude, + address: '', country: '', province: '', city: '', district: '', street: '', + cityCode: '', streetNum: '', adCode: '', aoiName: '', + accuracy: loc.accuracy, + locationType: 0, + errorCode: 0, + errorMsg: '', + }; + } + + private fillAddress(info: MaplocationInfo, a: geoLocationManager.GeoAddress): void { + info.address = a.placeName ?? ''; + info.country = a.countryName ?? ''; + info.province = a.administrativeArea ?? ''; + info.city = a.locality ?? ''; + info.district = a.subLocality ?? ''; + info.street = a.roadName ?? ''; + } + + private errorInfo(code: number): MaplocationInfo { + return { + latitude: 0, longitude: 0, + address: '', country: '', province: '', city: '', district: '', street: '', + cityCode: '', streetNum: '', adCode: '', aoiName: '', + accuracy: 0, locationType: 0, + errorCode: code, + errorMsg: code === -2 ? 'permission denied' : (code === -1 ? 'not ready' : 'location error'), + }; + } + + private push(info: MaplocationInfo): void { + this.bridge?.callHandler(OutboundHandlers.GetLocationInfo, JSON.stringify(info)); + if (!this.continuousOn) { + this.stopContinuous(); + } + } +} diff --git a/feature_capabilities/src/main/ets/providers/PhotoProvider.ets b/feature_capabilities/src/main/ets/providers/PhotoProvider.ets new file mode 100644 index 0000000..0ce49c8 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/PhotoProvider.ets @@ -0,0 +1,71 @@ +/** + * 图片下载能力(契约 §8.1/§9,T-M3-07)。复用 platform Downloader。 + * - getphoto:data=JSON 数组 [{pid, photourl(远程)}],批量下载到本地。 + * - 出站 getphoto:[{pid, photourl(本地 file://)}],全部下载完成后推送。 + * 落盘到资源根 photos/(在 file:// 跨域白名单内,H5 可经 引用)。 + */ +import { Downloader } from 'platform'; +import { ResourceManager } from 'domain_resource'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers, SavephotoURLBean } from 'contracts'; +import { common } from '@kit.AbilityKit'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +export class PhotoProvider implements CapabilityProvider { + readonly name: string = 'photo'; + private readonly log: Logger = Logger.tag('PhotoProvider'); + private bridge: BridgeController | undefined = undefined; + private context: common.UIAbilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.context = ctx.uiAbilityContext; + bridge.registerHandler(InboundHandlers.GetPhoto, (data: string, _cb: (resp: string) => void) => { + this.downloadAll(data); + }); + } + + private downloadAll(data: string): void { + const ctx = this.context; + if (ctx === undefined) { + return; + } + let list: SavephotoURLBean[]; + try { + list = JSON.parse(data) as SavephotoURLBean[]; + } catch (e) { + this.log.w(`getphoto bad json: ${(e as Error).message}`); + return; + } + const dir: string = `${ResourceManager.resourceRoot(ctx)}/photos`; + const tasks: Promise[] = list.map((item: SavephotoURLBean) => this.downloadOne(item, dir)); + Promise.all(tasks).then((results: SavephotoURLBean[]) => { + this.bridge?.callHandler(OutboundHandlers.GetPhoto, JSON.stringify(results)); + }).catch((e: Error) => { + this.log.w(`getphoto failed: ${e.message}`); + }); + } + + private async downloadOne(item: SavephotoURLBean, dir: string): Promise { + const local: string = `${dir}/${item.pid}${this.guessExt(item.photourl)}`; + try { + await Downloader.download(item.photourl, local); + return { pid: item.pid, photourl: `file://${local}` }; + } catch (e) { + this.log.w(`download photo ${item.pid} failed: ${(e as Error).message}`); + return { pid: item.pid, photourl: '' }; + } + } + + private guessExt(url: string): string { + const q: number = url.indexOf('?'); + const clean: string = q >= 0 ? url.substring(0, q) : url; + const dot: number = clean.lastIndexOf('.'); + const slash: number = clean.lastIndexOf('/'); + if (dot > slash && dot >= 0 && clean.length - dot <= 5) { + return clean.substring(dot); + } + return '.jpg'; + } +} diff --git a/feature_capabilities/src/main/ets/providers/ScanProvider.ets b/feature_capabilities/src/main/ets/providers/ScanProvider.ets new file mode 100644 index 0000000..cc824e6 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/ScanProvider.ets @@ -0,0 +1,42 @@ +/** + * 扫码能力(契约 §8.8/§9,T-M3-11)。@kit.ScanKit 默认界面扫码。 + * - opensaoma(拼写原样):拉起系统扫码界面 → 出站 getsaomaData(扫码结果裸串)。 + * 默认界面扫码由系统提供,无需申请相机权限。 + */ +import { scanBarcode } from '@kit.ScanKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { common } from '@kit.AbilityKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +export class ScanProvider implements CapabilityProvider { + readonly name: string = 'scan'; + private readonly log: Logger = Logger.tag('ScanProvider'); + private bridge: BridgeController | undefined = undefined; + private context: common.UIAbilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.bridge = bridge; + this.context = ctx.uiAbilityContext; + bridge.registerHandler(InboundHandlers.OpenSaoma, (_d: string, _cb: (resp: string) => void) => { + this.scan(); + }); + } + + private scan(): void { + const ctx = this.context; + if (ctx === undefined) { + return; + } + scanBarcode.startScanForResult(ctx) + .then((result: scanBarcode.ScanResult) => { + this.bridge?.callHandler(OutboundHandlers.GetSaomaData, result.originalValue ?? ''); + }) + .catch((e: BusinessError) => { + // 用户取消或失败:不推(H5 按"无事发生"处理),仅日志 + this.log.i(`scan canceled/failed: ${e.code} ${e.message}`); + }); + } +} diff --git a/feature_capabilities/src/main/ets/providers/ShakeProvider.ets b/feature_capabilities/src/main/ets/providers/ShakeProvider.ets new file mode 100644 index 0000000..d0a3187 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/ShakeProvider.ets @@ -0,0 +1,96 @@ +/** + * 摇一摇能力(契约 §8.5/§9,T-M3-08)。@kit.SensorServiceKit 加速度计。 + * - startshake:开始监听;检测到摇动后约 1 秒出站 shakeEnd(空串)。 + * - SwitchShake:data="1" 开声音/震动反馈 / 其他关。 + * - stopshake:停止监听。 + * 需 module.json5 声明 ohos.permission.ACCELEROMETER。 + */ +import { sensor } from '@kit.SensorServiceKit'; +import { vibrator } from '@kit.SensorServiceKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OutboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +/** 摇动阈值(合加速度 m/s²;静止约 9.8,摇动易超 20)。 */ +const SHAKE_THRESHOLD: number = 20; +/** 上报周期 100ms。 */ +const INTERVAL_NS: number = 100000000; +/** shakeEnd 延迟(约 1 秒)。 */ +const SHAKE_END_DELAY: number = 1000; + +export class ShakeProvider implements CapabilityProvider { + readonly name: string = 'shake'; + private readonly log: Logger = Logger.tag('ShakeProvider'); + private bridge: BridgeController | undefined = undefined; + private listening: boolean = false; + private feedbackOn: boolean = false; + private pending: boolean = false; + private readonly onAccel: (data: sensor.AccelerometerResponse) => void; + + constructor() { + this.onAccel = (data: sensor.AccelerometerResponse) => this.handleAccel(data); + } + + register(bridge: BridgeController, _ctx: CapabilityContext): void { + this.bridge = bridge; + bridge.registerHandler(InboundHandlers.StartShake, (_d: string, _cb: (resp: string) => void) => { + this.start(); + }); + bridge.registerHandler(InboundHandlers.SwitchShake, (data: string, _cb: (resp: string) => void) => { + this.feedbackOn = data === '1'; + }); + bridge.registerHandler(InboundHandlers.StopShake, (_d: string, _cb: (resp: string) => void) => { + this.stop(); + }); + } + + onDestroy(): void { + this.stop(); + } + + private start(): void { + if (this.listening) { + return; + } + try { + sensor.on(sensor.SensorId.ACCELEROMETER, this.onAccel, { interval: INTERVAL_NS }); + this.listening = true; + } catch (e) { + this.log.w(`sensor.on failed: ${(e as BusinessError).message}`); + } + } + + private stop(): void { + if (!this.listening) { + return; + } + try { + sensor.off(sensor.SensorId.ACCELEROMETER, this.onAccel); + } catch (e) { + this.log.w(`sensor.off failed: ${(e as BusinessError).message}`); + } + this.listening = false; + } + + private handleAccel(data: sensor.AccelerometerResponse): void { + if (this.pending) { + return; + } + const magnitude: number = Math.sqrt(data.x * data.x + data.y * data.y + data.z * data.z); + if (magnitude < SHAKE_THRESHOLD) { + return; + } + // 摇动触发:节流;可选反馈震动;约 1 秒后出站 shakeEnd + this.pending = true; + if (this.feedbackOn) { + vibrator.startVibration({ type: 'time', duration: 200 }, { usage: 'notification' }) + .catch((e: BusinessError) => this.log.w(`shake vibrate failed: ${e.message}`)); + } + setTimeout(() => { + this.bridge?.callHandler(OutboundHandlers.ShakeEnd, ''); + this.pending = false; + }, SHAKE_END_DELAY); + } +}