八角度审查双 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>
193 lines
6.8 KiB
Plaintext
193 lines
6.8 KiB
Plaintext
/**
|
||
* 定位能力(契约 §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));
|
||
}
|
||
}
|