Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0570ef8f7 | |||
| facde1fef7 | |||
| 38106a2096 | |||
| a476afb311 | |||
| db4c7b9b72 | |||
| 3bc490b485 | |||
| dd6d70c46e | |||
| b1eaf42fef | |||
| fb9e5dcd10 | |||
| f95e71463f | |||
| 1088bff43d |
@@ -13,6 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|||||||
import ChatScreen from './src/screens/ChatScreen';
|
import ChatScreen from './src/screens/ChatScreen';
|
||||||
import SettingsScreen from './src/screens/SettingsScreen';
|
import SettingsScreen from './src/screens/SettingsScreen';
|
||||||
import rvs from './src/services/rvs';
|
import rvs from './src/services/rvs';
|
||||||
|
import { initLogger } from './src/services/logger';
|
||||||
|
|
||||||
// --- Navigation ---
|
// --- Navigation ---
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ const TAB_ICONS: Record<string, { active: string; inactive: string }> = {
|
|||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
// Beim Start: gespeicherte RVS-Konfiguration laden und verbinden
|
// Beim Start: gespeicherte RVS-Konfiguration laden und verbinden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Verbose-Logging-Setting laden BEVOR andere Module loslegen.
|
||||||
|
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
||||||
|
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
||||||
|
initLogger().catch(() => {});
|
||||||
const initConnection = async () => {
|
const initConnection = async () => {
|
||||||
const config = await rvs.loadConfig();
|
const config = await rvs.loadConfig();
|
||||||
if (config) {
|
if (config) {
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10002
|
versionCode 10007
|
||||||
versionName "0.1.0.2"
|
versionName "0.1.0.7"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,58 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
promise.resolve(true)
|
promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
|
||||||
|
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
|
||||||
|
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
|
||||||
|
* GAIN beanspruchen — das System invalidiert dabei den haengenden Stack-
|
||||||
|
* Eintrag des anderen Players — und sofort wieder abandonen. Spotify
|
||||||
|
* bekommt den Focus-Gain und resumed.
|
||||||
|
*
|
||||||
|
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
|
||||||
|
* laesst den AudioFocusRequest haengen.
|
||||||
|
*/
|
||||||
|
@ReactMethod
|
||||||
|
fun kickReleaseMedia(promise: Promise) {
|
||||||
|
val am = audioManager()
|
||||||
|
if (am == null) {
|
||||||
|
promise.resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Async laufen lassen — wir wollen einen request, Pause, dann abandon.
|
||||||
|
// Ohne Pause merkt das System (und damit Spotify) die kurze Owner-
|
||||||
|
// Wechsel oft gar nicht. 250ms reicht erfahrungsgemaess fuer den
|
||||||
|
// Focus-Stack-Refresh.
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val attrs = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
|
.build()
|
||||||
|
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||||
|
val kickReq = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||||
|
.setAudioAttributes(attrs)
|
||||||
|
.setOnAudioFocusChangeListener(kickListener)
|
||||||
|
.build()
|
||||||
|
am.requestAudioFocus(kickReq)
|
||||||
|
Thread.sleep(250)
|
||||||
|
am.abandonAudioFocusRequest(kickReq)
|
||||||
|
} else {
|
||||||
|
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
am.requestAudioFocus(kickListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
|
||||||
|
Thread.sleep(250)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
am.abandonAudioFocus(kickListener)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "kickReleaseMedia: USAGE_MEDIA-Stack aufgemischt (250ms Pause)")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "kickReleaseMedia failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
private fun release() {
|
private fun release() {
|
||||||
val am = audioManager() ?: return
|
val am = audioManager() ?: return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.0.2",
|
"version": "0.1.0.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1038,19 +1038,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
{!isUser && item.text.length > 0 && (
|
{!isUser && item.text.length > 0 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.playButton}
|
style={styles.playButton}
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
if (item.audioPath) {
|
// Erst lokalen Cache pruefen — audioPath kann auf eine geloeschte
|
||||||
audioService.playFromPath(item.audioPath);
|
// Datei zeigen (TTS-Cache geleert oder Auto-Cleanup). In dem Fall
|
||||||
} else {
|
// ueber RVS neu rendern lassen statt stumm zu bleiben.
|
||||||
// messageId mitschicken damit die Bridge das generierte Audio
|
const cachePath = item.audioPath?.replace(/^file:\/\//, '') || '';
|
||||||
// wieder mit der Nachricht verknuepft (fuer den naechsten Replay aus Cache)
|
const cached = cachePath ? await RNFS.exists(cachePath).catch(() => false) : false;
|
||||||
rvs.send('tts_request' as any, {
|
if (cached) {
|
||||||
text: item.text,
|
audioService.playFromPath(item.audioPath!);
|
||||||
voice: localXttsVoiceRef.current,
|
return;
|
||||||
speed: ttsSpeedRef.current,
|
|
||||||
messageId: item.messageId || '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// messageId mitschicken damit die Bridge das generierte Audio
|
||||||
|
// wieder mit der Nachricht verknuepft (fuer den naechsten Replay aus Cache)
|
||||||
|
rvs.send('tts_request' as any, {
|
||||||
|
text: item.text,
|
||||||
|
voice: localXttsVoiceRef.current,
|
||||||
|
speed: ttsSpeedRef.current,
|
||||||
|
messageId: item.messageId || '',
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
|
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
TTS_SPEED_STORAGE_KEY,
|
TTS_SPEED_STORAGE_KEY,
|
||||||
} from '../services/audio';
|
} from '../services/audio';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
|
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
|
||||||
import {
|
import {
|
||||||
isWakeReadySoundEnabled,
|
isWakeReadySoundEnabled,
|
||||||
setWakeReadySoundEnabled,
|
setWakeReadySoundEnabled,
|
||||||
@@ -137,6 +138,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [showVadInfo, setShowVadInfo] = useState(false);
|
const [showVadInfo, setShowVadInfo] = useState(false);
|
||||||
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
||||||
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
|
||||||
|
const [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging());
|
||||||
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
||||||
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
||||||
const [wakeStatus, setWakeStatus] = useState<string>('');
|
const [wakeStatus, setWakeStatus] = useState<string>('');
|
||||||
@@ -1291,6 +1293,28 @@ const SettingsScreen: React.FC = () => {
|
|||||||
{/* === Logs === */}
|
{/* === Logs === */}
|
||||||
{currentSection === 'protocol' && (<>
|
{currentSection === 'protocol' && (<>
|
||||||
<Text style={styles.sectionTitle}>Protokoll</Text>
|
<Text style={styles.sectionTitle}>Protokoll</Text>
|
||||||
|
|
||||||
|
{/* Verbose-Logging-Toggle */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<Text style={styles.toggleLabel}>Verbose Logging</Text>
|
||||||
|
<Switch
|
||||||
|
value={verboseLogging}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setVerboseLogging(v);
|
||||||
|
setVerboseLoggingState(v);
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#3A3A52', true: '#0096FF' }}
|
||||||
|
thumbColor={verboseLogging ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Wenn aus: console.log wird global stummgeschaltet (Speicher schonen).
|
||||||
|
Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum
|
||||||
|
Debuggen via adb logcat.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
{/* Tab-Umschalter */}
|
{/* Tab-Umschalter */}
|
||||||
<View style={styles.tabRow}>
|
<View style={styles.tabRow}>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const { AudioFocus, PcmStreamPlayer } = NativeModules as {
|
|||||||
requestDuck: () => Promise<boolean>;
|
requestDuck: () => Promise<boolean>;
|
||||||
requestExclusive: () => Promise<boolean>;
|
requestExclusive: () => Promise<boolean>;
|
||||||
release: () => Promise<boolean>;
|
release: () => Promise<boolean>;
|
||||||
|
kickReleaseMedia: () => Promise<boolean>;
|
||||||
|
getMode?: () => Promise<number>;
|
||||||
};
|
};
|
||||||
PcmStreamPlayer?: {
|
PcmStreamPlayer?: {
|
||||||
start: (sampleRate: number, channels: number, prerollSeconds: number) => Promise<boolean>;
|
start: (sampleRate: number, channels: number, prerollSeconds: number) => Promise<boolean>;
|
||||||
@@ -316,13 +318,19 @@ class AudioService {
|
|||||||
* unterdrueckt — der Focus bleibt fuer die ganze Konversation gehalten. */
|
* unterdrueckt — der Focus bleibt fuer die ganze Konversation gehalten. */
|
||||||
private _releaseFocusDeferred(): void {
|
private _releaseFocusDeferred(): void {
|
||||||
if (this._conversationFocusActive) {
|
if (this._conversationFocusActive) {
|
||||||
|
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
|
console.log('[Audio] _releaseFocusDeferred: in %dms', this.FOCUS_RELEASE_DELAY_MS);
|
||||||
this.focusReleaseTimer = setTimeout(() => {
|
this.focusReleaseTimer = setTimeout(() => {
|
||||||
this.focusReleaseTimer = null;
|
this.focusReleaseTimer = null;
|
||||||
if (this._conversationFocusActive) return;
|
if (this._conversationFocusActive) {
|
||||||
|
console.log('[Audio] Focus-Release abgebrochen (Conversation jetzt aktiv)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[Audio] AudioFocus jetzt released');
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||||
}
|
}
|
||||||
@@ -1131,11 +1139,13 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.currentSound = sound;
|
this.currentSound = sound;
|
||||||
|
console.log('[Audio] Sound.play startet (path=%s)', soundPath);
|
||||||
|
|
||||||
// Naechstes Audio schon vorbereiten waehrend dieses abspielt
|
// Naechstes Audio schon vorbereiten waehrend dieses abspielt
|
||||||
this._preloadNext();
|
this._preloadNext();
|
||||||
|
|
||||||
sound.play((success) => {
|
sound.play((success) => {
|
||||||
|
console.log('[Audio] Sound.play callback: success=%s queue=%d', success, this.audioQueue.length);
|
||||||
if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen');
|
if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen');
|
||||||
sound.release();
|
sound.release();
|
||||||
this.currentSound = null;
|
this.currentSound = null;
|
||||||
@@ -1181,6 +1191,12 @@ class AudioService {
|
|||||||
|
|
||||||
/** Laufende Wiedergabe stoppen + Queue leeren */
|
/** Laufende Wiedergabe stoppen + Queue leeren */
|
||||||
stopPlayback(): void {
|
stopPlayback(): void {
|
||||||
|
// Idempotent: wenn nichts mehr aktiv ist, NICHT noch einen Focus-Release/
|
||||||
|
// Kick-Cycle anstossen — Re-Renders triggern setMuted oft mehrfach hinter-
|
||||||
|
// einander, und jeder weitere Kick lässt Spotify nochmal kurz pausieren.
|
||||||
|
const hasAnything = !!(this.currentSound || this.resumeSound || this.preloadedSound
|
||||||
|
|| this.pcmStreamActive || this.audioQueue.length || this.isPlaying);
|
||||||
|
if (!hasAnything) return;
|
||||||
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
||||||
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
||||||
// Foreground-Service auch stoppen — sonst bleibt die Notification haengen
|
// Foreground-Service auch stoppen — sonst bleibt die Notification haengen
|
||||||
@@ -1216,6 +1232,10 @@ class AudioService {
|
|||||||
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
|
// Focus-Stack immer aufmischen — bei aelteren Nachrichten die ueber
|
||||||
|
// tts_request (PCM-Stream) re-rendert wurden, bleibt Spotify ohne den
|
||||||
|
// Kick auch pausiert. Kostet nichts, deckt beide Pfade ab.
|
||||||
|
AudioFocus?.kickReleaseMedia?.().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Status & Callbacks ---
|
// --- Status & Callbacks ---
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Verbose-Logging-Toggle: console.log laesst sich global stummschalten.
|
||||||
|
* console.warn/console.error bleiben immer an — Fehler will man immer sehen.
|
||||||
|
*
|
||||||
|
* Default: an (true). Toggle ueber Settings → Protokoll → Verbose Logging.
|
||||||
|
* Beim Start wird der gespeicherte Wert geladen, vorher loggen wir normal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||||
|
|
||||||
|
// Original-console.log retten, damit wir die Wrapper jederzeit wieder
|
||||||
|
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
|
||||||
|
const originalLog = console.log.bind(console);
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
let _verbose = true;
|
||||||
|
|
||||||
|
function applyState(): void {
|
||||||
|
console.log = _verbose ? originalLog : noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wert aus AsyncStorage laden und anwenden. Beim App-Start aufrufen. */
|
||||||
|
export async function initLogger(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
|
||||||
|
_verbose = v !== 'false'; // default: true
|
||||||
|
} catch {}
|
||||||
|
applyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVerboseLogging(): boolean {
|
||||||
|
return _verbose;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setVerboseLogging(verbose: boolean): void {
|
||||||
|
_verbose = verbose;
|
||||||
|
applyState();
|
||||||
|
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user