diff --git a/entry/src/main/ets/di/AppModule.ets b/entry/src/main/ets/di/AppModule.ets index e1cebb5..9971120 100644 --- a/entry/src/main/ets/di/AppModule.ets +++ b/entry/src/main/ets/di/AppModule.ets @@ -5,16 +5,20 @@ * M2:仅示例能力(验证插件模式)。M3:替换为全部真实 Provider + 占位桩 * (RoomStubProvider/PayStubProvider,§6.5),契约 handler 名/数量不变。 */ -import { CapabilityProvider, EchoSampleProvider } from 'feature_capabilities'; +import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider, + VibrateProvider, ClipboardProvider, AppSystemProvider } from 'feature_capabilities'; export function buildCapabilities(): CapabilityProvider[] { return [ - new EchoSampleProvider(), - // —— M3 在此装配(顺序无关,互不依赖)—— - // new ShareProvider(), new LoginProvider(), new LocationProvider(), new AudioProvider(), - // new ShakeProvider(), new DeviceProvider(), new ClipboardProvider(), new NetworkProvider(), - // new VibrateProvider(), new ScanProvider(), new CameraProvider(), new PhotoProvider(), - // new NavProvider(), new AppSystemProvider(), - // new RoomStubProvider(), new PayStubProvider(), // 暂缓能力占位桩(§6.5) + new EchoSampleProvider(), // M2 回显样板(M3 全部 Provider 就位后可移除) + // —— M3 已落地能力(顺序无关,互不依赖)—— + new AppSystemProvider(), // orientation/browser/finsh/openApplyDownloadpath/notification + new VibrateProvider(), // vibrator/repeatvibrator/canclevibrator + new ClipboardProvider(), // gameCopytext/gamepastetext + // —— 暂缓能力占位桩(§6.5)—— + new RoomStubProvider(), + new PayStubProvider(), + // —— M3 待落地:DeviceProvider/NetworkProvider/ShakeProvider/LocationProvider/AudioProvider/ + // ScanProvider/CameraProvider/PhotoProvider/NavProvider/ShareProvider/LoginProvider —— ]; } diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index b1d4582..3ea8b00 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -55,6 +55,9 @@ "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.VIBRATE" } ] } diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index f27dd6e..d5eaa9f 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -4,5 +4,11 @@ export { CapabilityProvider, CapabilityContext } from './src/main/ets/core/CapabilityProvider'; export { CapabilityRegistrar } from './src/main/ets/core/CapabilityRegistrar'; -// 示例能力(M2 脚手架,M3 替换为真实 Provider) +// 示例能力(M2 脚手架,M3 逐步替换为真实 Provider) export { EchoSampleProvider } from './src/main/ets/providers/EchoSampleProvider'; + +// —— M3 能力 Provider —— +export { RoomStubProvider, PayStubProvider } from './src/main/ets/providers/StubProviders'; +export { VibrateProvider } from './src/main/ets/providers/VibrateProvider'; +export { ClipboardProvider } from './src/main/ets/providers/ClipboardProvider'; +export { AppSystemProvider } from './src/main/ets/providers/AppSystemProvider'; diff --git a/feature_capabilities/src/main/ets/providers/AppSystemProvider.ets b/feature_capabilities/src/main/ets/providers/AppSystemProvider.ets new file mode 100644 index 0000000..2e922f2 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/AppSystemProvider.ets @@ -0,0 +1,86 @@ +/** + * 系统/应用能力(契约 §8.4/§8.8/§8.10,T-M3-01)。 + * - orientation:data="1" 横屏 / 其他 竖屏(窗口动态切换,§7.4 保留 H5 显式方向控制)。 + * - browser:data=URL,隐式 want 拉起系统浏览器。 + * - finsh(拼写原样):退出当前 Ability。 + * - openApplyDownloadpath:data=JSON{packagename,downloadpath},按包名启动 App,失败则浏览器打开下载地址。 + * - notification:空实现(契约标注未落地)。 + */ +import { common, Want } from '@kit.AbilityKit'; +import { window } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers, OpenApplyDownloadReq } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +export class AppSystemProvider implements CapabilityProvider { + readonly name: string = 'appsystem'; + private readonly log: Logger = Logger.tag('AppSystemProvider'); + private context: common.UIAbilityContext | undefined = undefined; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + this.context = ctx.uiAbilityContext; + bridge.registerHandler(InboundHandlers.Orientation, (data: string, _cb: (resp: string) => void) => { + this.setOrientation(data === '1'); + }); + bridge.registerHandler(InboundHandlers.Browser, (data: string, _cb: (resp: string) => void) => { + this.openBrowser(data); + }); + bridge.registerHandler(InboundHandlers.Finsh, (_d: string, _cb: (resp: string) => void) => { + this.context?.terminateSelf() + .catch((e: BusinessError) => this.log.w(`terminateSelf failed: ${e.code} ${e.message}`)); + }); + bridge.registerHandler(InboundHandlers.OpenApplyDownloadPath, (data: string, _cb: (resp: string) => void) => { + this.openApply(data); + }); + bridge.registerHandler(InboundHandlers.Notification, (_d: string, _cb: (resp: string) => void) => { + // 契约标注:空实现,未落地 + }); + } + + /** 动态设方向:横屏 LANDSCAPE / 竖屏 PORTRAIT(对齐 Android setRequestedOrientation)。 */ + private setOrientation(landscape: boolean): void { + const ctx = this.context; + if (ctx === undefined) { + return; + } + const target: window.Orientation = landscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT; + window.getLastWindow(ctx) + .then((win: window.Window) => win.setPreferredOrientation(target)) + .catch((e: BusinessError) => this.log.w(`setOrientation failed: ${e.code} ${e.message}`)); + } + + private openBrowser(url: string): void { + if (this.context === undefined || url === '') { + return; + } + const want: Want = { + action: 'ohos.want.action.viewData', + entities: ['entity.system.browsable'], + uri: url, + }; + this.context.startAbility(want) + .catch((e: BusinessError) => this.log.w(`openBrowser failed: ${e.code} ${e.message}`)); + } + + private openApply(data: string): void { + const ctx = this.context; + if (ctx === undefined) { + return; + } + let req: OpenApplyDownloadReq; + try { + req = JSON.parse(data) as OpenApplyDownloadReq; + } catch (e) { + this.log.w(`openApply bad json: ${(e as Error).message}`); + return; + } + // 按包名启动 App;失败回退浏览器打开下载地址 + ctx.startAbility({ bundleName: req.packagename } as Want) + .catch((e: BusinessError) => { + this.log.i(`start app ${req.packagename} failed (${e.code}); fallback to download page`); + this.openBrowser(req.downloadpath); + }); + } +} diff --git a/feature_capabilities/src/main/ets/providers/ClipboardProvider.ets b/feature_capabilities/src/main/ets/providers/ClipboardProvider.ets new file mode 100644 index 0000000..4e086e6 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/ClipboardProvider.ets @@ -0,0 +1,47 @@ +/** + * 剪贴板能力(契约 §8.4,T-M3-05)。@kit.BasicServicesKit pasteboard。 + * - gameCopytext:data=文本,写入系统剪贴板。 + * - gamepastetext:读取剪贴板内容,经 responseCallback 返回(getData 异步,回调延后触发,对 H5 仍是同步回执语义)。 + * 前台应用读取自身剪贴板无需声明权限;失败回空串(H5 不卡死)。 + */ +import { pasteboard } from '@kit.BasicServicesKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +export class ClipboardProvider implements CapabilityProvider { + readonly name: string = 'clipboard'; + private readonly log: Logger = Logger.tag('ClipboardProvider'); + + register(bridge: BridgeController, _ctx: CapabilityContext): void { + bridge.registerHandler(InboundHandlers.GameCopyText, (data: string, _cb: (resp: string) => void) => { + this.copy(data); + }); + bridge.registerHandler(InboundHandlers.GamePasteText, (_d: string, cb: (resp: string) => void) => { + this.paste(cb); + }); + } + + private copy(text: string): void { + try { + const data: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); + pasteboard.getSystemPasteboard().setData(data) + .catch((e: BusinessError) => this.log.w(`setData failed: ${e.code} ${e.message}`)); + } catch (e) { + this.log.w(`copy failed: ${(e as Error).message}`); + } + } + + private paste(cb: (resp: string) => void): void { + pasteboard.getSystemPasteboard().getData() + .then((data: pasteboard.PasteData) => { + cb(data.getPrimaryText() ?? ''); + }) + .catch((e: BusinessError) => { + this.log.w(`getData failed: ${e.code} ${e.message}`); + cb(''); + }); + } +} diff --git a/feature_capabilities/src/main/ets/providers/StubProviders.ets b/feature_capabilities/src/main/ets/providers/StubProviders.ets new file mode 100644 index 0000000..24e9249 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/StubProviders.ets @@ -0,0 +1,41 @@ +/** + * 暂缓能力占位桩(框架 §6.5)。本期视频房(声网)、支付不集成,但 handler 必须"留空注册"—— + * 否则 H5 调用行为不确定,若 H5 传了 responseCallback 还可能永久挂起。 + * + * 规则: + * - void 型 handler:空实现 + 日志,**绝不**调用任何 callHandler 出站。 + * - 有同步返回的:回安全默认值(本桩涉及的均为 void 型,无同步返回)。 + * - 绝不触发该能力出站(不 callHandler getVideoinfo / PayuserPaytypePaystate)。 + * 未来换回真实 RoomProvider/PayProvider 时,契约 handler 名/数量不变,对 H5 零改动。 + */ +import { BridgeController, BridgeHandler } from 'feature_bridge'; +import { InboundHandlers } from 'contracts'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +/** 视频房占位桩(声网,§6.5)。 */ +export class RoomStubProvider implements CapabilityProvider { + readonly name: string = 'room(stub)'; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + const noop: BridgeHandler = (_d: string, _cb: (resp: string) => void) => { + ctx.log.i('room capability not integrated (stub)'); + }; + bridge.registerHandler(InboundHandlers.CreateRoom, noop); + bridge.registerHandler(InboundHandlers.ExitRoom, noop); + bridge.registerHandler(InboundHandlers.GetVideoInfo, noop); + bridge.registerHandler(InboundHandlers.DragViewVideoIsShow, noop); + } +} + +/** 支付占位桩(本期支付暂缓,§6.5)。 */ +export class PayStubProvider implements CapabilityProvider { + readonly name: string = 'pay(stub)'; + + register(bridge: BridgeController, ctx: CapabilityContext): void { + const noop: BridgeHandler = (_d: string, _cb: (resp: string) => void) => { + ctx.log.i('pay capability not integrated (stub)'); + }; + bridge.registerHandler(InboundHandlers.PayBrowser, noop); + bridge.registerHandler(InboundHandlers.GetGameplay, noop); + } +} diff --git a/feature_capabilities/src/main/ets/providers/VibrateProvider.ets b/feature_capabilities/src/main/ets/providers/VibrateProvider.ets new file mode 100644 index 0000000..b23c70e --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/VibrateProvider.ets @@ -0,0 +1,81 @@ +/** + * 震动能力(契约 §8.4,T-M3-04)。@kit.SensorServiceKit vibrator。 + * - vibrator:data=毫秒裸串,单次震动。 + * - repeatvibrator:data="1" 重复 / 其他停止重复。 + * - canclevibrator(拼写原样):取消全部震动。 + * 需 module.json5 声明 ohos.permission.VIBRATE。 + */ +import { vibrator } from '@kit.SensorServiceKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { BridgeController } from 'feature_bridge'; +import { InboundHandlers } from 'contracts'; +import { Logger } from 'common'; +import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; + +const DEFAULT_DURATION: number = 1000; + +export class VibrateProvider implements CapabilityProvider { + readonly name: string = 'vibrate'; + private readonly log: Logger = Logger.tag('VibrateProvider'); + private lastDuration: number = DEFAULT_DURATION; + private repeating: boolean = false; + private repeatTimer: number = -1; + + register(bridge: BridgeController, _ctx: CapabilityContext): void { + bridge.registerHandler(InboundHandlers.Vibrator, (data: string, _cb: (resp: string) => void) => { + this.vibrateOnce(this.parseMs(data)); + }); + bridge.registerHandler(InboundHandlers.RepeatVibrator, (data: string, _cb: (resp: string) => void) => { + if (data === '1') { + this.startRepeat(); + } else { + this.stopRepeat(); + } + }); + bridge.registerHandler(InboundHandlers.CancleVibrator, (_d: string, _cb: (resp: string) => void) => { + this.cancel(); + }); + } + + onDestroy(): void { + this.cancel(); + } + + private parseMs(data: string): number { + const n: number = parseInt(data, 10); + return isNaN(n) || n <= 0 ? DEFAULT_DURATION : n; + } + + private vibrateOnce(ms: number): void { + this.lastDuration = ms; + vibrator.startVibration({ type: 'time', duration: ms }, { usage: 'notification' }) + .catch((e: BusinessError) => this.log.w(`startVibration failed: ${e.code} ${e.message}`)); + } + + private startRepeat(): void { + this.repeating = true; + this.loop(); + } + + private loop(): void { + if (!this.repeating) { + return; + } + this.vibrateOnce(this.lastDuration); + // 当次震动结束后再触发下一次(间隔取等长),形成重复,直到取消 + this.repeatTimer = setTimeout(() => this.loop(), this.lastDuration * 2); + } + + private stopRepeat(): void { + this.repeating = false; + if (this.repeatTimer !== -1) { + clearTimeout(this.repeatTimer); + this.repeatTimer = -1; + } + } + + private cancel(): void { + this.stopRepeat(); + vibrator.stopVibration().catch((e: BusinessError) => this.log.w(`stopVibration failed: ${e.code} ${e.message}`)); + } +}