129 lines
5 KiB
TypeScript
129 lines
5 KiB
TypeScript
// Typed wrapper around the self-managed Telecom call plugin
|
|
// (TelecomCallPlugin.java + VojoCallsManager.kt). See
|
|
// docs/plans/telecom_migration.md.
|
|
//
|
|
// Telecom gives Vojo's native Android calls system-level call interop (a GSM
|
|
// call interrupts/holds the Vojo call and vice-versa), audio-focus
|
|
// arbitration, Bluetooth / Android Auto / Wear answer + route surfaces, and a
|
|
// system "ongoing call" presence. The actual media stays in the Element Call
|
|
// WebView; this layer only drives the Telecom call STATE and translates its
|
|
// callbacks (answer / disconnect / hold / mute / route) back to JS.
|
|
//
|
|
// Telecom is the sole native call backend on Android (minSdk 26 = CallsManager
|
|
// is always available; the legacy AudioRoute path has been retired). There is
|
|
// no opt-in flag any more — it is active on every Android build.
|
|
//
|
|
// Android-only. On web / iOS every method is a no-op and never touches the
|
|
// native plugin.
|
|
|
|
import { registerPlugin, type PluginListenerHandle } from '@capacitor/core';
|
|
import { isAndroidPlatform } from '../../utils/capacitor';
|
|
|
|
/** Audio routes mirrored to/from CallEndpointCompat types (see VojoCallsManager). */
|
|
export type TelecomRoute = 'EARPIECE' | 'SPEAKER' | 'BLUETOOTH' | 'WIRED' | 'STREAMING' | 'UNKNOWN';
|
|
|
|
/** Events emitted by TelecomCallPlugin (see VojoCallsManager.Listener). */
|
|
export type TelecomEvent =
|
|
| 'telecomAnswer'
|
|
| 'telecomDisconnect'
|
|
| 'telecomSetActive'
|
|
| 'telecomSetInactive'
|
|
| 'telecomMute'
|
|
| 'telecomEndpoint'
|
|
| 'telecomEndpoints'
|
|
| 'telecomError';
|
|
|
|
export interface TelecomStartOptions {
|
|
roomId: string;
|
|
displayName: string;
|
|
video?: boolean;
|
|
incoming?: boolean;
|
|
}
|
|
|
|
export interface TelecomCallPlugin {
|
|
startCall(options: TelecomStartOptions): Promise<void>;
|
|
answer(options: { roomId: string; video?: boolean }): Promise<void>;
|
|
setActive(): Promise<void>;
|
|
endCall(): Promise<void>;
|
|
requestEndpoint(options: { route: TelecomRoute }): Promise<void>;
|
|
addListener(
|
|
eventName: 'telecomAnswer',
|
|
listenerFunc: (data: { video: boolean }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomDisconnect',
|
|
listenerFunc: (data: { causeCode: number }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomSetActive' | 'telecomSetInactive',
|
|
listenerFunc: () => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomMute',
|
|
listenerFunc: (data: { muted: boolean }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomEndpoint',
|
|
listenerFunc: (data: { route: TelecomRoute }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomEndpoints',
|
|
listenerFunc: (data: { routes: TelecomRoute[] }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
addListener(
|
|
eventName: 'telecomError',
|
|
listenerFunc: (data: { message: string }) => void
|
|
): Promise<PluginListenerHandle>;
|
|
}
|
|
|
|
const plugin = registerPlugin<TelecomCallPlugin>('TelecomCall');
|
|
|
|
/** True on Android, where Telecom is the native call backend (no-op elsewhere). */
|
|
export const isTelecomEnabled = (): boolean => isAndroidPlatform();
|
|
|
|
export const telecomCall = {
|
|
enabled: isTelecomEnabled,
|
|
|
|
// Begin a Telecom session alongside the WebView call. Outgoing goes active
|
|
// immediately; incoming stays ringing until answer().
|
|
startCall(options: TelecomStartOptions): Promise<void> {
|
|
if (!isTelecomEnabled()) return Promise.resolve();
|
|
return plugin.startCall(options);
|
|
},
|
|
// Move a ring-time incoming Telecom session (RINGING → ACTIVE). roomId guards
|
|
// against answering the wrong session when multiple rings exist. No-op when
|
|
// Telecom is off or no native session exists for the room.
|
|
answer(roomId: string, video = false): Promise<void> {
|
|
if (!isTelecomEnabled()) return Promise.resolve();
|
|
return plugin.answer({ roomId, video });
|
|
},
|
|
setActive(): Promise<void> {
|
|
if (!isTelecomEnabled()) return Promise.resolve();
|
|
return plugin.setActive();
|
|
},
|
|
// Idempotent best-effort teardown — safe to call on any platform.
|
|
endCall(): Promise<void> {
|
|
if (!isTelecomEnabled()) return Promise.resolve();
|
|
return plugin.endCall();
|
|
},
|
|
requestEndpoint(route: TelecomRoute): Promise<void> {
|
|
if (!isTelecomEnabled()) return Promise.resolve();
|
|
return plugin.requestEndpoint({ route });
|
|
},
|
|
|
|
// Subscribe to a Telecom event. Returns a no-op handle when disabled so
|
|
// callers can unconditionally register + clean up. (Plain union event type:
|
|
// TS `Parameters<>` on the overloaded plugin signature only captures the last
|
|
// overload, so we don't derive the event name from it.)
|
|
addListener(
|
|
eventName: TelecomEvent,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
listenerFunc: (data: any) => void
|
|
): Promise<PluginListenerHandle> {
|
|
if (!isTelecomEnabled()) {
|
|
return Promise.resolve({ remove: () => Promise.resolve() });
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return plugin.addListener(eventName as any, listenerFunc);
|
|
},
|
|
};
|