Files
youle_app_ohos/feature_capabilities/src/main/ets/providers/LocationProvider.ets
T
lanterngamescn 9be1a23635 review: M5 双 Web 审查后的质量加固(两轮真机验证)
八角度审查双 Web 批次。修确凿问题:

铁律风险(最高)
- network_config:base-config cleartextTrafficPermitted 由 false 改 true。我之前的"白名单收紧"比平台默认更严,
  会拦掉 H5 可能访问的任意第三方明文 http 与本地 127.0.0.1 上传 → 违反 H5 零改动。改回允许明文(对齐原 Android)

正确性
- 子游戏 controller 不再复用:每次进子游戏 new WebviewController()(存 slotS.controller),消除"复用同一 controller
  时 onControllerAttached 不再触发致第二个子游戏建不出桥"的竞态。真机两轮验证 subB 桥正常 ✓
- BridgeController.dispose 置 active=false:显式抑制 dispose 后能力延迟异步 callHandler(不靠 adapter emitter-off 偶然安全)
- WebSlot.resumeLoaded 补发 setPostUrl:覆盖"非激活期渲染崩溃重载后 onFirstProgress 被闸门抑制丢失上传地址"。真机验证返回必带 setPostUrl ✓
- onRenderExited 不可见时延迟重载:非激活槽崩溃标记 needReload,激活时再 reload(官方建议可见后重载);activate 返回是否重载,resumeLoaded 据此避免重复推送
- 容器 aboutToDisappear:先退订 cancels 再 dispose 槽,关闭销毁期事件又建槽窗口
- LocationProvider:权限对话框 await 后加 disposed 守卫(防 onDestroy 后再注册监听泄漏+打已弃桥)
- AudioProvider:createAVPlayer.then 加 disposed 守卫(创建期已销毁则直接 release,防孤儿播放器泄漏)
- NetworkProvider.onForeground:重订阅后补播当前网络(覆盖非激活期网络变化,避免 H5 状态陈旧)

研判驳回:network_config 需 metadata 关联(官方文档证实 profile 路径自动识别);app 前后台只发 activeSlot(非激活大厅已收 appservice'2',正确)。
延期:WeChatApi 单例陈旧回调(随微信集成)。

devecocli build + scripts/test.sh 通过;模拟器两轮(大厅↔subA↔大厅↔subB↔大厅)无回归。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:13:44 +08:00

193 lines
6.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 定位能力(契约 §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 wantContinuous: boolean = false;
private disposed: 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)));
});
}
/** 槽激活:恢复连续定位(若 H5 仍要)。 */
onForeground(): void {
if (this.wantContinuous) {
this.startContinuous();
}
}
/** 槽去激活:停连续定位监听(保留 H5 意图)。 */
onBackground(): void {
this.stopContinuous();
}
onDestroy(): void {
this.disposed = true;
this.wantContinuous = false;
this.stopContinuous();
}
private async start(continuous: boolean): Promise<void> {
const ctx = this.context;
if (ctx === undefined) {
return;
}
const granted: boolean = await PermissionGuard.request(ctx, LOC_PERMS);
// 权限对话框期间槽可能已销毁:勿在 onDestroy 后再注册监听(否则泄漏 + 打已弃桥)
if (this.disposed) {
return;
}
if (!granted) {
this.lastInfo = this.errorInfo(-2);
this.push(this.lastInfo);
return;
}
this.wantContinuous = continuous;
if (continuous) {
this.startContinuous();
} else {
// 单次请求前,若已有连续监听在跑则先停,避免单次后仍持续推送(H5 预期一次)
this.stopContinuous();
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 {
// 单次定位用 getCurrentLocation(一次性,无监听可停);连续定位由 stopshake 等价的 stopContinuous 管理。
this.bridge?.callHandler(OutboundHandlers.GetLocationInfo, JSON.stringify(info));
}
}