- Remove speaker button empty space (2-button centered layout) - Remove "Asteria voice" text from voice call screen - Fix chat input visibility with keyboard - Add keyboard show listener for auto-scroll 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
269 lines
7.7 KiB
TypeScript
269 lines
7.7 KiB
TypeScript
/**
|
|
* Android Voice Call Service
|
|
*
|
|
* Handles:
|
|
* 1. Foreground Service notification - keeps call alive in background
|
|
* 2. Battery Optimization check - warns user if optimization is enabled
|
|
*
|
|
* Only runs on Android - iOS handles background audio differently.
|
|
*/
|
|
|
|
import { Platform, Alert, Linking, NativeModules } from 'react-native';
|
|
|
|
// Notifee for foreground service
|
|
let notifee: any = null;
|
|
|
|
/**
|
|
* Lazy load notifee to avoid issues on iOS
|
|
*/
|
|
async function getNotifee() {
|
|
if (Platform.OS !== 'android') return null;
|
|
|
|
if (!notifee) {
|
|
try {
|
|
notifee = (await import('@notifee/react-native')).default;
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to load notifee:', e);
|
|
return null;
|
|
}
|
|
}
|
|
return notifee;
|
|
}
|
|
|
|
// Channel ID for voice call notifications
|
|
const CHANNEL_ID = 'voice-call-channel';
|
|
const NOTIFICATION_ID = 'voice-call-active';
|
|
|
|
/**
|
|
* Create notification channel (required for Android 8+)
|
|
*/
|
|
async function createNotificationChannel(): Promise<void> {
|
|
const notifeeModule = await getNotifee();
|
|
if (!notifeeModule) return;
|
|
|
|
try {
|
|
await notifeeModule.createChannel({
|
|
id: CHANNEL_ID,
|
|
name: 'Voice Calls',
|
|
description: 'Notifications for active voice calls with Julia AI',
|
|
importance: 4, // HIGH - shows notification but no sound
|
|
vibration: false,
|
|
sound: undefined,
|
|
});
|
|
console.log('[AndroidVoiceService] Notification channel created');
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to create channel:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start foreground service with notification
|
|
* Call this when voice call starts
|
|
*/
|
|
export async function startVoiceCallService(): Promise<void> {
|
|
if (Platform.OS !== 'android') {
|
|
console.log('[AndroidVoiceService] Skipping - not Android');
|
|
return;
|
|
}
|
|
|
|
console.log('[AndroidVoiceService] Starting foreground service...');
|
|
|
|
const notifeeModule = await getNotifee();
|
|
if (!notifeeModule) {
|
|
console.log('[AndroidVoiceService] Notifee not available');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create channel first
|
|
await createNotificationChannel();
|
|
|
|
// Display foreground service notification
|
|
await notifeeModule.displayNotification({
|
|
id: NOTIFICATION_ID,
|
|
title: 'Julia AI - Call Active',
|
|
body: 'Voice call in progress. Tap to return to the app.',
|
|
android: {
|
|
channelId: CHANNEL_ID,
|
|
asForegroundService: true,
|
|
ongoing: true, // Can't be swiped away
|
|
autoCancel: false,
|
|
smallIcon: 'ic_notification', // Uses default if not found
|
|
color: '#22c55e', // Green color
|
|
pressAction: {
|
|
id: 'default',
|
|
launchActivity: 'default',
|
|
},
|
|
// Important for keeping audio alive
|
|
importance: 4, // HIGH
|
|
category: 2, // CATEGORY_CALL
|
|
},
|
|
});
|
|
|
|
console.log('[AndroidVoiceService] Foreground service started');
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to start foreground service:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop foreground service
|
|
* Call this when voice call ends
|
|
*/
|
|
export async function stopVoiceCallService(): Promise<void> {
|
|
if (Platform.OS !== 'android') return;
|
|
|
|
console.log('[AndroidVoiceService] Stopping foreground service...');
|
|
|
|
const notifeeModule = await getNotifee();
|
|
if (!notifeeModule) return;
|
|
|
|
try {
|
|
await notifeeModule.stopForegroundService();
|
|
await notifeeModule.cancelNotification(NOTIFICATION_ID);
|
|
console.log('[AndroidVoiceService] Foreground service stopped');
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to stop foreground service:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if battery optimization is disabled for our app
|
|
* Returns true if optimization is DISABLED (good for us)
|
|
* Returns false if optimization is ENABLED (bad - system may kill our app)
|
|
*/
|
|
export async function isBatteryOptimizationDisabled(): Promise<boolean> {
|
|
if (Platform.OS !== 'android') {
|
|
return true; // iOS doesn't need this
|
|
}
|
|
|
|
try {
|
|
const notifeeModule = await getNotifee();
|
|
if (!notifeeModule) return true; // Assume OK if can't check
|
|
|
|
// Notifee provides a way to check power manager settings
|
|
const powerManagerInfo = await notifeeModule.getPowerManagerInfo();
|
|
|
|
// If device has power manager restrictions
|
|
if (powerManagerInfo.activity) {
|
|
return false; // Battery optimization is likely enabled
|
|
}
|
|
|
|
return true;
|
|
} catch (e) {
|
|
console.log('[AndroidVoiceService] Could not check battery optimization:', e);
|
|
return true; // Assume OK on error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open battery optimization settings for our app
|
|
*/
|
|
export async function openBatteryOptimizationSettings(): Promise<void> {
|
|
if (Platform.OS !== 'android') return;
|
|
|
|
try {
|
|
const notifeeModule = await getNotifee();
|
|
if (notifeeModule) {
|
|
// Try to open power manager settings via notifee
|
|
await notifeeModule.openPowerManagerSettings();
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.log('[AndroidVoiceService] Notifee openPowerManagerSettings failed:', e);
|
|
}
|
|
|
|
// Fallback: try to open battery optimization settings directly
|
|
try {
|
|
// Try generic battery settings
|
|
await Linking.openSettings();
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to open settings:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show alert about battery optimization
|
|
* Call this before starting a voice call on Android
|
|
*/
|
|
export function showBatteryOptimizationAlert(): void {
|
|
if (Platform.OS !== 'android') return;
|
|
|
|
Alert.alert(
|
|
'Optimize for Voice Calls',
|
|
'To ensure voice calls continue working when the app is in the background, please disable battery optimization for WellNuo.\n\nThis prevents Android from stopping the call when you switch apps or lock your screen.',
|
|
[
|
|
{
|
|
text: 'Later',
|
|
style: 'cancel',
|
|
},
|
|
{
|
|
text: 'Open Settings',
|
|
onPress: () => openBatteryOptimizationSettings(),
|
|
},
|
|
],
|
|
{ cancelable: true }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check battery optimization and show alert if needed
|
|
* Returns true if we should proceed with the call
|
|
* Returns false if user chose to go to settings (call should be postponed)
|
|
*/
|
|
export async function checkAndPromptBatteryOptimization(): Promise<boolean> {
|
|
if (Platform.OS !== 'android') {
|
|
return true; // iOS - proceed
|
|
}
|
|
|
|
const isDisabled = await isBatteryOptimizationDisabled();
|
|
|
|
if (isDisabled) {
|
|
console.log('[AndroidVoiceService] Battery optimization already disabled - good!');
|
|
return true;
|
|
}
|
|
|
|
// Show alert and wait for user response
|
|
return new Promise((resolve) => {
|
|
Alert.alert(
|
|
'Optimize for Voice Calls',
|
|
'For reliable voice calls in the background, we recommend disabling battery optimization for WellNuo.\n\nWould you like to adjust this setting now?',
|
|
[
|
|
{
|
|
text: 'Skip for Now',
|
|
style: 'cancel',
|
|
onPress: () => resolve(true), // Proceed anyway
|
|
},
|
|
{
|
|
text: 'Open Settings',
|
|
onPress: async () => {
|
|
await openBatteryOptimizationSettings();
|
|
resolve(false); // Don't start call - user went to settings
|
|
},
|
|
},
|
|
],
|
|
{ cancelable: false }
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Request notification permission (required for Android 13+)
|
|
*/
|
|
export async function requestNotificationPermission(): Promise<boolean> {
|
|
if (Platform.OS !== 'android') return true;
|
|
|
|
const notifeeModule = await getNotifee();
|
|
if (!notifeeModule) return false;
|
|
|
|
try {
|
|
const settings = await notifeeModule.requestPermission();
|
|
const granted = settings.authorizationStatus >= 1; // AUTHORIZED or PROVISIONAL
|
|
console.log('[AndroidVoiceService] Notification permission:', granted ? 'granted' : 'denied');
|
|
return granted;
|
|
} catch (e) {
|
|
console.error('[AndroidVoiceService] Failed to request notification permission:', e);
|
|
return false;
|
|
}
|
|
}
|