feat(release): close handler and microphone gaps
This commit is contained in:
@@ -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');
|
||||
}
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
/** 权限被拒时的中文引导文案。 */
|
||||
|
||||
Reference in New Issue
Block a user