feat(release): close handler and microphone gaps

This commit is contained in:
key
2026-07-30 12:01:45 +08:00
parent 8934f13811
commit c16a3e83d7
30 changed files with 830 additions and 49 deletions
+55 -6
View File
@@ -8,14 +8,25 @@
* Android 运行时授权框。已授权设备上本调用为无害空转;若确实不弹,则它是唯一的授权入口。
*/
import { withPlatformPermissionActivity } from '../services/file-selection-lifecycle';
const RECORD_AUDIO = 'android.permission.RECORD_AUDIO';
/** 授权申请超时:回调未触发时不能无限挂起,放行交由 getUserMedia 兜底。 */
const REQUEST_TIMEOUT_MS = 15000;
/** Android 权限 Activity 返回后,必须等应用窗口重新获得焦点再启动录音或文件选择。 */
const ACTIVITY_FOCUS_POLL_MS = 50;
const ACTIVITY_FOCUS_TIMEOUT_MS = 1500;
interface AndroidActivityLike {
hasWindowFocus?: () => boolean;
}
export type MicPermissionResult = 'granted' | 'denied' | 'denied-always' | 'unsupported';
interface PlusAndroidLike {
importClass?: (target: unknown) => unknown;
runtimeMainActivity?: () => AndroidActivityLike;
requestPermissions?: (
permissions: string[],
success: (event: { granted?: string[]; deniedPresent?: string[]; deniedAlways?: string[] }) => void,
@@ -28,6 +39,42 @@ const androidBridge = (): PlusAndroidLike | null => {
return plusApi?.android ?? null;
};
const settleAfterPermissionActivity = (
android: PlusAndroidLike,
settle: (result: MicPermissionResult) => void,
result: MicPermissionResult
) => {
let activity: AndroidActivityLike | undefined;
try {
activity = android.runtimeMainActivity?.();
if (activity) android.importClass?.(activity);
} catch {
// 某些旧基座不暴露主 Activity;仍至少跨过当前权限回调任务。
}
if (!activity?.hasWindowFocus) {
setTimeout(() => settle(result), ACTIVITY_FOCUS_POLL_MS);
return;
}
const deadline = Date.now() + ACTIVITY_FOCUS_TIMEOUT_MS;
const poll = () => {
let focused = false;
try {
focused = Boolean(activity?.hasWindowFocus?.());
} catch {
settle(result);
return;
}
if (focused || Date.now() >= deadline) {
settle(result);
return;
}
setTimeout(poll, ACTIVITY_FOCUS_POLL_MS);
};
// 即使回调时已经恢复焦点,也跨过一次事件循环,避免和权限 Activity 的 finish 竞争。
setTimeout(poll, ACTIVITY_FOCUS_POLL_MS);
};
/**
* 主动申请麦克风权限。
* 非 Android App 运行时(H5、iOS)返回 unsupported,由各自平台的原生授权流程处理。
@@ -36,7 +83,7 @@ export const requestMicrophonePermission = (): Promise<MicPermissionResult> => {
const android = androidBridge();
if (!android?.requestPermissions) return Promise.resolve('unsupported');
return new Promise((resolve) => {
return withPlatformPermissionActivity(() => new Promise((resolve) => {
let settled = false;
const settle = (result: MicPermissionResult) => {
if (settled) return;
@@ -50,17 +97,19 @@ export const requestMicrophonePermission = (): Promise<MicPermissionResult> => {
android.requestPermissions!(
[RECORD_AUDIO],
(event) => {
if (event?.granted?.length) settle('granted');
else if (event?.deniedAlways?.length) settle('denied-always');
else if (event?.deniedPresent?.length) settle('denied');
else settle('denied');
const result: MicPermissionResult = event?.granted?.length
? 'granted'
: event?.deniedAlways?.length
? 'denied-always'
: 'denied';
settleAfterPermissionActivity(android, settle, result);
},
() => settle('unsupported')
);
} catch {
settle('unsupported');
}
});
}));
};
/** 权限被拒时的中文引导文案。 */