Skip to content

Commit c0b2575

Browse files
committed
RU-T47 PR#209 fixes
1 parent 12c9f08 commit c0b2575

21 files changed

Lines changed: 517 additions & 409 deletions

.DS_Store

0 Bytes
Binary file not shown.

app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
8181
'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE',
8282
'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
8383
'android.permission.READ_PHONE_STATE',
84-
'android.permission.READ_PHONE_NUMBERS',
8584
'android.permission.MANAGE_OWN_CALLS',
8685
],
8786
},
@@ -211,6 +210,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
211210
'./plugins/withForegroundNotifications.js',
212211
'./plugins/withNotificationSounds.js',
213212
'./plugins/withMediaButtonModule.js',
213+
'./plugins/withInCallAudioModule.js',
214214
['app-icon-badge', appIconBadgeConfig],
215215
],
216216
extra: {

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@
168168
"react-native-restart": "0.0.27",
169169
"react-native-safe-area-context": "5.4.0",
170170
"react-native-screens": "~4.11.1",
171-
"react-native-sound": "^0.13.0",
172171
"react-native-svg": "15.11.2",
173172
"react-native-web": "^0.20.0",
174173
"react-native-webview": "~13.13.1",

plugins/withInCallAudioModule.js

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
const { withDangerousMod, withMainApplication } = require('@expo/config-plugins');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
/**
6+
* Android InCallAudioModule.kt content
7+
* Uses SoundPool to play sounds on the VOICE_COMMUNICATION stream.
8+
*/
9+
const ANDROID_MODULE = `package {{PACKAGE_NAME}}
10+
11+
import android.content.Context
12+
import android.media.AudioAttributes
13+
import android.media.AudioManager
14+
import android.media.SoundPool
15+
import android.os.Build
16+
import android.util.Log
17+
import com.facebook.react.bridge.*
18+
19+
class InCallAudioModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
20+
21+
companion object {
22+
private const val TAG = "InCallAudioModule"
23+
}
24+
25+
private var soundPool: SoundPool? = null
26+
private val soundMap = HashMap<String, Int>()
27+
private val loadedSounds = HashSet<Int>()
28+
private var isInitialized = false
29+
30+
override fun getName(): String {
31+
return "InCallAudioModule"
32+
}
33+
34+
@ReactMethod
35+
fun initializeAudio() {
36+
if (isInitialized) return
37+
38+
val audioAttributes = AudioAttributes.Builder()
39+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
40+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
41+
.build()
42+
43+
soundPool = SoundPool.Builder()
44+
.setMaxStreams(1)
45+
.setAudioAttributes(audioAttributes)
46+
.build()
47+
48+
soundPool?.setOnLoadCompleteListener { _, sampleId, status ->
49+
if (status == 0) {
50+
loadedSounds.add(sampleId)
51+
Log.d(TAG, "Sound loaded successfully: $sampleId")
52+
} else {
53+
Log.e(TAG, "Failed to load sound $sampleId, status: $status")
54+
}
55+
}
56+
57+
isInitialized = true
58+
Log.d(TAG, "InCallAudioModule initialized with USAGE_VOICE_COMMUNICATION")
59+
}
60+
61+
@ReactMethod
62+
fun loadSound(name: String, resourceName: String) {
63+
if (!isInitialized) initializeAudio()
64+
65+
val context = reactApplicationContext
66+
var resId = context.resources.getIdentifier(resourceName, "raw", context.packageName)
67+
68+
// Fallback: Try identifying without package name if first attempt fails (though context.packageName is usually correct)
69+
if (resId == 0) {
70+
Log.w(TAG, "Resource $resourceName not found in \${context.packageName}, trying simplified lookup")
71+
// Reflection-based lookup if needed, but getIdentifier is standard.
72+
}
73+
74+
if (resId != 0) {
75+
soundPool?.let { pool ->
76+
val soundId = pool.load(context, resId, 1)
77+
soundMap[name] = soundId
78+
Log.d(TAG, "Loading sound: $name from resource: $resourceName (id: $soundId, resId: $resId)")
79+
}
80+
} else {
81+
Log.e(TAG, "Resource not found: $resourceName in package \${context.packageName}")
82+
}
83+
}
84+
85+
@ReactMethod
86+
fun playSound(name: String) {
87+
val soundId = soundMap[name]
88+
if (soundId != null) {
89+
if (loadedSounds.contains(soundId)) {
90+
val streamId = soundPool?.play(soundId, 0.5f, 0.5f, 1, 0, 1.0f)
91+
if (streamId == 0) {
92+
Log.e(TAG, "Failed to play sound: $name (id: $soundId). StreamId is 0. Check Volume/Focus.")
93+
} else {
94+
Log.d(TAG, "Playing sound: $name (id: $soundId, stream: $streamId)")
95+
}
96+
} else {
97+
Log.w(TAG, "Sound $name (id: $soundId) is not ready yet. Ignoring play request.")
98+
}
99+
} else {
100+
Log.w(TAG, "Sound not found in map: $name")
101+
}
102+
}
103+
104+
@ReactMethod
105+
fun cleanup() {
106+
soundPool?.release()
107+
soundPool = null
108+
soundMap.clear()
109+
loadedSounds.clear()
110+
isInitialized = false
111+
Log.d(TAG, "InCallAudioModule cleaned up")
112+
}
113+
}
114+
`;
115+
116+
/**
117+
* Android InCallAudioPackage.kt content
118+
*/
119+
const ANDROID_PACKAGE = `package {{PACKAGE_NAME}}
120+
121+
import android.view.View
122+
import com.facebook.react.ReactPackage
123+
import com.facebook.react.bridge.NativeModule
124+
import com.facebook.react.bridge.ReactApplicationContext
125+
import com.facebook.react.uimanager.ReactShadowNode
126+
import com.facebook.react.uimanager.ViewManager
127+
128+
class InCallAudioPackage : ReactPackage {
129+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
130+
return listOf(InCallAudioModule(reactContext))
131+
}
132+
133+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<View, ReactShadowNode<*>>> {
134+
return emptyList()
135+
}
136+
}
137+
`;
138+
139+
/**
140+
* Helper to resolve package name
141+
*/
142+
function resolveBasePackageName(projectRoot, fallback = 'com.resgrid.unit') {
143+
const namespaceRegex = /namespace\s*(?:=)?\s*['"]([^'"]+)['"]/;
144+
145+
const groovyPath = path.join(projectRoot, 'android', 'app', 'build.gradle');
146+
if (fs.existsSync(groovyPath)) {
147+
const content = fs.readFileSync(groovyPath, 'utf-8');
148+
const match = content.match(namespaceRegex);
149+
if (match) return match[1];
150+
}
151+
152+
const ktsPath = path.join(projectRoot, 'android', 'app', 'build.gradle.kts');
153+
if (fs.existsSync(ktsPath)) {
154+
const content = fs.readFileSync(ktsPath, 'utf-8');
155+
const match = content.match(namespaceRegex);
156+
if (match) return match[1];
157+
}
158+
159+
return fallback;
160+
}
161+
162+
const withInCallAudioModule = (config) => {
163+
// 1. Copy Assets to Android res/raw
164+
config = withDangerousMod(config, [
165+
'android',
166+
async (config) => {
167+
const projectRoot = config.modRequest.projectRoot;
168+
const resRawPath = path.join(projectRoot, 'android', 'app', 'src', 'main', 'res', 'raw');
169+
170+
if (!fs.existsSync(resRawPath)) {
171+
fs.mkdirSync(resRawPath, { recursive: true });
172+
}
173+
174+
const assets = ['software_interface_start.mp3', 'software_interface_back.mp3', 'positive_interface_beep.mp3', 'space_notification1.mp3', 'space_notification2.mp3'];
175+
176+
const sourceBase = path.join(projectRoot, 'assets', 'audio', 'ui');
177+
178+
assets.forEach((filename) => {
179+
const sourcePath = path.join(sourceBase, filename);
180+
const destPath = path.join(resRawPath, filename);
181+
182+
if (fs.existsSync(sourcePath)) {
183+
fs.copyFileSync(sourcePath, destPath);
184+
console.log(`[withInCallAudioModule] Copied ${filename} to res/raw/${filename}`);
185+
} else {
186+
console.warn(`[withInCallAudioModule] Source audio file not found: ${sourcePath}`);
187+
}
188+
});
189+
190+
return config;
191+
},
192+
]);
193+
194+
// 2. Add Native Module Code
195+
config = withDangerousMod(config, [
196+
'android',
197+
async (config) => {
198+
const projectRoot = config.modRequest.projectRoot;
199+
const packageName = resolveBasePackageName(projectRoot);
200+
const packagePath = packageName.replace(/\./g, '/');
201+
const androidSrcPath = path.join(projectRoot, 'android', 'app', 'src', 'main', 'java', packagePath);
202+
203+
if (!fs.existsSync(androidSrcPath)) {
204+
fs.mkdirSync(androidSrcPath, { recursive: true });
205+
}
206+
207+
// InCallAudioModule.kt
208+
const modulePath = path.join(androidSrcPath, 'InCallAudioModule.kt');
209+
const moduleContent = ANDROID_MODULE.replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
210+
fs.writeFileSync(modulePath, moduleContent);
211+
console.log('[withInCallAudioModule] Created InCallAudioModule.kt');
212+
213+
// InCallAudioPackage.kt
214+
const packageFilePath = path.join(androidSrcPath, 'InCallAudioPackage.kt');
215+
const packageContent = ANDROID_PACKAGE.replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
216+
fs.writeFileSync(packageFilePath, packageContent);
217+
console.log('[withInCallAudioModule] Created InCallAudioPackage.kt');
218+
219+
return config;
220+
},
221+
]);
222+
223+
// 3. Register Package in MainApplication.kt
224+
config = withMainApplication(config, (config) => {
225+
const mainApplication = config.modResults;
226+
const projectRoot = config.modRequest.projectRoot;
227+
228+
if (!mainApplication.contents.includes('InCallAudioPackage')) {
229+
const basePackageName = resolveBasePackageName(projectRoot);
230+
const importStatement = `import ${basePackageName}.InCallAudioPackage`;
231+
232+
if (!mainApplication.contents.includes(importStatement)) {
233+
mainApplication.contents = mainApplication.contents.replace(/^(package\s+[^\n]+\n)/, `$1${importStatement}\n`);
234+
}
235+
236+
const packagesPattern = /val packages = PackageList\(this\)\.packages(\.toMutableList\(\))?/;
237+
const packagesMatch = mainApplication.contents.match(packagesPattern);
238+
239+
if (packagesMatch) {
240+
// Using the simplest replacement that ensures toMutableList()
241+
const replacement = `val packages = PackageList(this).packages.toMutableList()\n packages.add(InCallAudioPackage())`;
242+
243+
// Avoid double adding if MediaButtonPackage logic already changed it to mutable
244+
if (mainApplication.contents.includes('packages.add(MediaButtonPackage()')) {
245+
// Add ours after MediaButtonPackage
246+
mainApplication.contents = mainApplication.contents.replace('packages.add(MediaButtonPackage())', 'packages.add(MediaButtonPackage())\n packages.add(InCallAudioPackage())');
247+
} else {
248+
// Standard replacement
249+
mainApplication.contents = mainApplication.contents.replace(packagesPattern, replacement);
250+
}
251+
console.log('[withInCallAudioModule] Registered InCallAudioPackage in MainApplication.kt');
252+
}
253+
}
254+
255+
return config;
256+
});
257+
258+
return config;
259+
};
260+
261+
module.exports = withInCallAudioModule;

src/app/login/login-form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,5 @@ export const LoginForm = ({ onSubmit = () => {}, isLoading = false, error = unde
182182
</KeyboardAvoidingView>
183183
);
184184
};
185+
186+
export default LoginForm;

0 commit comments

Comments
 (0)