M3(批1): 占位桩 + 震动 + 剪贴板 + 系统能力 Provider

T-M3-15 RoomStubProvider/PayStubProvider:createRoom/exitRoom/getVideoinfo/DragViewvideoIsshow、
        paybrowser/getGameplay 全 void 空实现+日志,绝不触发出站(§6.5 安全网)
T-M3-04 VibrateProvider:vibrator/repeatvibrator/canclevibrator(@ohos.vibrator,重复用递归 setTimeout)
T-M3-05 ClipboardProvider:gameCopytext/gamepastetext(@ohos.pasteboard,读取经 responseCallback 回执)
T-M3-01 AppSystemProvider:orientation(window.setPreferredOrientation 横/竖)、browser(隐式 want)、
        finsh(terminateSelf)、openApplyDownloadpath(按包名启动失败回退浏览器)、notification(空)
组装根 buildCapabilities 装配上述 + 桩;module.json5 增 VIBRATE 权限

devecocli build 通过。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-25 19:15:21 +08:00
parent 4590d5bf70
commit 59b000ce7a
7 changed files with 277 additions and 9 deletions
+12 -8
View File
@@ -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 ——
];
}
+3
View File
@@ -55,6 +55,9 @@
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.VIBRATE"
}
]
}
+7 -1
View File
@@ -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';
@@ -0,0 +1,86 @@
/**
* 系统/应用能力(契约 §8.4/§8.8/§8.10T-M3-01)。
* - orientationdata="1" 横屏 / 其他 竖屏(窗口动态切换,§7.4 保留 H5 显式方向控制)。
* - browserdata=URL,隐式 want 拉起系统浏览器。
* - finsh(拼写原样):退出当前 Ability。
* - openApplyDownloadpathdata=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);
});
}
}
@@ -0,0 +1,47 @@
/**
* 剪贴板能力(契约 §8.4T-M3-05)。@kit.BasicServicesKit pasteboard。
* - gameCopytextdata=文本,写入系统剪贴板。
* - 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('');
});
}
}
@@ -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);
}
}
@@ -0,0 +1,81 @@
/**
* 震动能力(契约 §8.4T-M3-04)。@kit.SensorServiceKit vibrator。
* - vibratordata=毫秒裸串,单次震动。
* - repeatvibratordata="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}`));
}
}