@cloudflare/realtimekit-react-native 0.2.1 → 0.2.2-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/android/src/main/java/com/cloudflare/realtimekit/CoreModule.java +19 -0
  2. package/android/src/main/java/com/cloudflare/realtimekit/ForegroundService.java +2 -4
  3. package/android/src/main/java/com/cloudflare/realtimekit/RTKHelperModule.java +1 -2
  4. package/android/src/main/java/com/cloudflare/realtimekit/RTKLogger.java +116 -0
  5. package/android/src/main/java/com/cloudflare/realtimekit/incallmanager/AppRTCBluetoothManager.java +75 -74
  6. package/android/src/main/java/com/cloudflare/realtimekit/incallmanager/AppRTCUtils.java +10 -9
  7. package/android/src/main/java/com/cloudflare/realtimekit/incallmanager/InCallManagerModule.java +175 -872
  8. package/android/src/main/java/com/cloudflare/realtimekit/incallmanager/InCallProximityManager.java +9 -8
  9. package/android/src/main/java/com/cloudflare/realtimekit/incallmanager/InCallWakeLockUtils.java +8 -7
  10. package/ios/BroadcastEventEmitter.m +9 -0
  11. package/ios/Core.h +9 -1
  12. package/ios/Core.m +42 -0
  13. package/ios/RNInCallManager.m +639 -700
  14. package/ios/RTKLogger.h +51 -0
  15. package/ios/RTKLogger.m +107 -0
  16. package/ios/RTKRNBackgroundTimer.m +8 -7
  17. package/ios/RTKRNPermissionHandlerNotifications.m +12 -0
  18. package/ios/RTKRNPermissions.m +24 -1
  19. package/ios/RTKScreensharePickerView.m +6 -0
  20. package/ios/screenshare/RTKScreenshareHandler.swift +1 -1
  21. package/ios/screenshare/RTKScreenshareUploader.swift +3 -3
  22. package/ios/screenshare/RTKSocketConnection.swift +8 -8
  23. package/lib/commonjs/LocalMediaHandler.js +82 -24
  24. package/lib/commonjs/LocalMediaHandler.js.map +1 -1
  25. package/lib/commonjs/LocalMediaUtils.js +60 -5
  26. package/lib/commonjs/LocalMediaUtils.js.map +1 -1
  27. package/lib/commonjs/NativeAudioManager.js +7 -3
  28. package/lib/commonjs/NativeAudioManager.js.map +1 -1
  29. package/lib/commonjs/utils/version.js +1 -1
  30. package/lib/commonjs/utils/version.js.map +1 -1
  31. package/lib/module/LocalMediaHandler.js +82 -24
  32. package/lib/module/LocalMediaHandler.js.map +1 -1
  33. package/lib/module/LocalMediaUtils.js +60 -5
  34. package/lib/module/LocalMediaUtils.js.map +1 -1
  35. package/lib/module/NativeAudioManager.js +7 -3
  36. package/lib/module/NativeAudioManager.js.map +1 -1
  37. package/lib/module/utils/version.js +1 -1
  38. package/lib/module/utils/version.js.map +1 -1
  39. package/lib/typescript/LocalMediaHandler.d.ts +1 -0
  40. package/lib/typescript/LocalMediaUtils.d.ts +1 -0
  41. package/lib/typescript/NativeAudioManager.d.ts +1 -0
  42. package/lib/typescript/utils/version.d.ts +1 -1
  43. package/package.json +2 -4
@@ -10,27 +10,19 @@ import android.media.AudioAttributes;
10
10
  import android.media.AudioDeviceInfo;
11
11
  import android.media.AudioFocusRequest;
12
12
  import android.media.AudioManager;
13
- import android.media.MediaPlayer;
14
- import android.media.ToneGenerator;
15
- import android.net.Uri;
16
13
  import android.os.PowerManager;
17
14
  import android.os.Build;
18
15
  import android.os.Handler;
19
16
  import android.os.Looper;
20
- import android.provider.Settings;
21
17
  import androidx.annotation.Nullable;
22
18
  import android.util.Log;
23
- import android.util.SparseArray;
24
19
  import android.view.Display;
25
20
  import android.view.KeyEvent;
26
21
  import android.view.Window;
27
22
  import android.view.WindowManager;
28
- import android.media.AudioDeviceInfo;
29
- import android.media.MediaPlayer;
30
23
 
31
24
  import com.facebook.react.bridge.Arguments;
32
25
  import com.facebook.react.bridge.LifecycleEventListener;
33
- import com.facebook.react.bridge.NativeModule;
34
26
  import com.facebook.react.bridge.Promise;
35
27
  import com.facebook.react.bridge.ReactApplicationContext;
36
28
  import com.facebook.react.bridge.ReactContext;
@@ -40,16 +32,11 @@ import com.facebook.react.bridge.UiThreadUtil;
40
32
  import com.facebook.react.bridge.WritableMap;
41
33
  import com.facebook.react.modules.core.DeviceEventManagerModule;
42
34
 
43
- import java.lang.Runnable;
44
- import java.io.File;
45
35
  import java.util.Collections;
46
- import java.util.Map;
47
- import java.util.HashMap;
48
36
  import java.util.HashSet;
49
- import java.util.Random;
50
37
  import java.util.Set;
51
- import java.util.List;
52
38
 
39
+ import com.cloudflare.realtimekit.RTKLogger;
53
40
  import com.cloudflare.realtimekit.incallmanager.AppRTCBluetoothManager;
54
41
  import com.cloudflare.realtimekit.incallmanager.InCallManagerModule.AudioDevice;
55
42
 
@@ -72,7 +59,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
72
59
  private boolean origIsSpeakerPhoneOn = false;
73
60
  private boolean origIsMicrophoneMute = false;
74
61
  private int origAudioMode = AudioManager.MODE_INVALID;
75
- private boolean defaultSpeakerOn = false;
62
+ private boolean defaultSpeakerOn = true; // Default to video call mode (speaker on)
76
63
  private int defaultAudioMode = AudioManager.MODE_IN_COMMUNICATION;
77
64
  private int forceSpeakerOn = 0;
78
65
  private boolean automatic = true;
@@ -85,25 +72,6 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
85
72
  private AudioAttributes mAudioAttributes;
86
73
  private AudioFocusRequest mAudioFocusRequest;
87
74
 
88
- // --- same as: RingtoneManager.getActualDefaultRingtoneUri(reactContext, RingtoneManager.TYPE_RINGTONE);
89
- private Uri defaultRingtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
90
- private Uri defaultRingbackUri = Settings.System.DEFAULT_RINGTONE_URI;
91
- private Uri defaultBusytoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
92
- //private Uri defaultAlarmAlertUri = Settings.System.DEFAULT_ALARM_ALERT_URI; // --- too annoying
93
- private Uri bundleRingtoneUri;
94
- private Uri bundleRingbackUri;
95
- private Uri bundleBusytoneUri;
96
- private Map<String, Uri> audioUriMap;
97
- private MyPlayerInterface mRingtone;
98
- private MyPlayerInterface mRingback;
99
- private MyPlayerInterface mBusytone;
100
- private Handler mRingtoneCountDownHandler;
101
- private String media = "audio";
102
-
103
- private static final String SPEAKERPHONE_AUTO = "auto";
104
- private static final String SPEAKERPHONE_TRUE = "true";
105
- private static final String SPEAKERPHONE_FALSE = "false";
106
-
107
75
  /**
108
76
  * AudioDevice is the names of possible audio devices that we currently
109
77
  * support.
@@ -140,7 +108,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
140
108
  private AudioDevice userSelectedAudioDevice;
141
109
 
142
110
  // Contains speakerphone setting: auto, true or false
143
- private final String useSpeakerphone = SPEAKERPHONE_AUTO;
111
+ private final String useSpeakerphone = "SPEAKERPHONE_AUTO";
144
112
 
145
113
  // Handles all tasks related to Bluetooth headset devices.
146
114
  private AppRTCBluetoothManager bluetoothManager = null;
@@ -153,12 +121,6 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
153
121
  // avoid duplicate elements.
154
122
  private Set<AudioDevice> audioDevices = new HashSet<>();
155
123
 
156
- interface MyPlayerInterface {
157
- public boolean isPlaying();
158
- public void startPlay(Map<String, Object> data);
159
- public void stopPlay();
160
- }
161
-
162
124
  @Override
163
125
  public String getName() {
164
126
  return REACT_NATIVE_MODULE_NAME;
@@ -171,13 +133,6 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
171
133
  mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
172
134
  mPowerManager = (PowerManager) reactContext.getSystemService(Context.POWER_SERVICE);
173
135
  audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE));
174
- audioUriMap = new HashMap<String, Uri>();
175
- audioUriMap.put("defaultRingtoneUri", defaultRingtoneUri);
176
- audioUriMap.put("defaultRingbackUri", defaultRingbackUri);
177
- audioUriMap.put("defaultBusytoneUri", defaultBusytoneUri);
178
- audioUriMap.put("bundleRingtoneUri", bundleRingtoneUri);
179
- audioUriMap.put("bundleRingbackUri", bundleRingbackUri);
180
- audioUriMap.put("bundleBusytoneUri", bundleBusytoneUri);
181
136
  wakeLockUtils = new InCallWakeLockUtils(reactContext);
182
137
  proximityManager = InCallProximityManager.create(reactContext, this);
183
138
 
@@ -185,16 +140,16 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
185
140
  bluetoothManager = AppRTCBluetoothManager.create(reactContext, this);
186
141
  });
187
142
 
188
- // Log.d(TAG, "InCallManager initialized");
143
+ RTKLogger.d("InCallManager", "InCallManager initialized");
189
144
  }
190
145
 
191
146
  private void manualTurnScreenOff() {
192
- // Log.d(TAG, "manualTurnScreenOff()");
147
+ RTKLogger.d("InCallManager", "manualTurnScreenOff()");
193
148
  UiThreadUtil.runOnUiThread(new Runnable() {
194
149
  public void run() {
195
150
  Activity mCurrentActivity = getCurrentActivity();
196
151
  if (mCurrentActivity == null) {
197
- // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
152
+ RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
198
153
  return;
199
154
  }
200
155
  Window window = mCurrentActivity.getWindow();
@@ -208,12 +163,12 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
208
163
  }
209
164
 
210
165
  private void manualTurnScreenOn() {
211
- // Log.d(TAG, "manualTurnScreenOn()");
166
+ RTKLogger.d("InCallManager", "manualTurnScreenOn()");
212
167
  UiThreadUtil.runOnUiThread(new Runnable() {
213
168
  public void run() {
214
169
  Activity mCurrentActivity = getCurrentActivity();
215
170
  if (mCurrentActivity == null) {
216
- // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
171
+ RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
217
172
  return;
218
173
  }
219
174
  Window window = mCurrentActivity.getWindow();
@@ -230,7 +185,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
230
185
  }
231
186
 
232
187
  private void storeOriginalAudioSetup() {
233
- // Log.d(TAG, "storeOriginalAudioSetup()");
188
+ RTKLogger.d("InCallManager", "storeOriginalAudioSetup()");
234
189
  if (!isOrigAudioSetupStored) {
235
190
  origAudioMode = audioManager.getMode();
236
191
  origIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
@@ -240,7 +195,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
240
195
  }
241
196
 
242
197
  private void restoreOriginalAudioSetup() {
243
- // Log.d(TAG, "restoreOriginalAudioSetup()");
198
+ RTKLogger.d("InCallManager", "restoreOriginalAudioSetup()");
244
199
  if (isOrigAudioSetupStored) {
245
200
  setSpeakerphoneOn(origIsSpeakerPhoneOn);
246
201
  setMicrophoneMute(origIsMicrophoneMute);
@@ -254,7 +209,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
254
209
 
255
210
  private void startWiredHeadsetEvent() {
256
211
  if (wiredHeadsetReceiver == null) {
257
- // Log.d(TAG, "startWiredHeadsetEvent()");
212
+ RTKLogger.d("InCallManager", "startWiredHeadsetEvent()");
258
213
  IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG);
259
214
  wiredHeadsetReceiver = new BroadcastReceiver() {
260
215
  @Override
@@ -270,7 +225,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
270
225
  data.putBoolean("isPlugged", (intent.getIntExtra("state", 0) == 1) ? true : false);
271
226
  data.putBoolean("hasMic", (intent.getIntExtra("microphone", 0) == 1) ? true : false);
272
227
  data.putString("deviceName", deviceName);
273
- sendEvent("WiredHeadset", data);
228
+ sendEvent("onAudioDeviceChanged", null);
274
229
  } else {
275
230
  hasWiredHeadset = false;
276
231
  }
@@ -280,14 +235,14 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
280
235
  if (reactContext != null) {
281
236
  this.registerReceiver(wiredHeadsetReceiver, filter);
282
237
  } else {
283
- // Log.d(TAG, "startWiredHeadsetEvent() reactContext is null");
238
+ RTKLogger.w("InCallManager", "startWiredHeadsetEvent() reactContext is null");
284
239
  }
285
240
  }
286
241
  }
287
242
 
288
243
  private void stopWiredHeadsetEvent() {
289
244
  if (wiredHeadsetReceiver != null) {
290
- // Log.d(TAG, "stopWiredHeadsetEvent()");
245
+ RTKLogger.d("InCallManager", "stopWiredHeadsetEvent()");
291
246
  this.unregisterReceiver(this.wiredHeadsetReceiver);
292
247
  wiredHeadsetReceiver = null;
293
248
  }
@@ -295,7 +250,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
295
250
 
296
251
  private void startNoisyAudioEvent() {
297
252
  if (noisyAudioReceiver == null) {
298
- // Log.d(TAG, "startNoisyAudioEvent()");
253
+ RTKLogger.d("InCallManager", "startNoisyAudioEvent()");
299
254
  IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
300
255
  noisyAudioReceiver = new BroadcastReceiver() {
301
256
  @Override
@@ -310,87 +265,22 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
310
265
  if (reactContext != null) {
311
266
  this.registerReceiver(noisyAudioReceiver, filter);
312
267
  } else {
313
- // Log.d(TAG, "startNoisyAudioEvent() reactContext is null");
268
+ RTKLogger.w("InCallManager", "startNoisyAudioEvent() reactContext is null");
314
269
  }
315
270
  }
316
271
  }
317
272
 
318
273
  private void stopNoisyAudioEvent() {
319
274
  if (noisyAudioReceiver != null) {
320
- // Log.d(TAG, "stopNoisyAudioEvent()");
275
+ RTKLogger.d("InCallManager", "stopNoisyAudioEvent()");
321
276
  this.unregisterReceiver(this.noisyAudioReceiver);
322
277
  noisyAudioReceiver = null;
323
278
  }
324
279
  }
325
280
 
326
- private void startMediaButtonEvent() {
327
- if (mediaButtonReceiver == null) {
328
- // Log.d(TAG, "startMediaButtonEvent()");
329
- IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
330
- mediaButtonReceiver = new BroadcastReceiver() {
331
- @Override
332
- public void onReceive(Context context, Intent intent) {
333
- if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
334
- KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
335
- int keyCode = event.getKeyCode();
336
- String keyText = "";
337
- switch (keyCode) {
338
- case KeyEvent.KEYCODE_MEDIA_PLAY:
339
- keyText = "KEYCODE_MEDIA_PLAY";
340
- break;
341
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
342
- keyText = "KEYCODE_MEDIA_PAUSE";
343
- break;
344
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
345
- keyText = "KEYCODE_MEDIA_PLAY_PAUSE";
346
- break;
347
- case KeyEvent.KEYCODE_MEDIA_NEXT:
348
- keyText = "KEYCODE_MEDIA_NEXT";
349
- break;
350
- case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
351
- keyText = "KEYCODE_MEDIA_PREVIOUS";
352
- break;
353
- case KeyEvent.KEYCODE_MEDIA_CLOSE:
354
- keyText = "KEYCODE_MEDIA_CLOSE";
355
- break;
356
- case KeyEvent.KEYCODE_MEDIA_EJECT:
357
- keyText = "KEYCODE_MEDIA_EJECT";
358
- break;
359
- case KeyEvent.KEYCODE_MEDIA_RECORD:
360
- keyText = "KEYCODE_MEDIA_RECORD";
361
- break;
362
- case KeyEvent.KEYCODE_MEDIA_STOP:
363
- keyText = "KEYCODE_MEDIA_STOP";
364
- break;
365
- default:
366
- keyText = "KEYCODE_UNKNOW";
367
- break;
368
- }
369
- WritableMap data = Arguments.createMap();
370
- data.putString("eventText", keyText);
371
- data.putInt("eventCode", keyCode);
372
- sendEvent("MediaButton", data);
373
- }
374
- }
375
- };
376
- ReactContext reactContext = getReactApplicationContext();
377
- if (reactContext != null) {
378
- this.registerReceiver(mediaButtonReceiver, filter);
379
- } else {
380
- // Log.d(TAG, "startMediaButtonEvent() reactContext is null");
381
- }
382
- }
383
- }
384
-
385
- private void stopMediaButtonEvent() {
386
- if (mediaButtonReceiver != null) {
387
- // Log.d(TAG, "stopMediaButtonEvent()");
388
- this.unregisterReceiver(this.mediaButtonReceiver);
389
- mediaButtonReceiver = null;
390
- }
391
- }
392
-
393
281
  public void onProximitySensorChangedState(boolean isNear) {
282
+ RTKLogger.d("InCallManager", "onProximitySensorChangedState() isNear=" + isNear + ", selectedAudioDevice=" + getSelectedAudioDevice());
283
+
394
284
  if (automatic && getSelectedAudioDevice() == AudioDevice.EARPIECE) {
395
285
  if (isNear) {
396
286
  turnScreenOff();
@@ -407,33 +297,33 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
407
297
  @ReactMethod
408
298
  public void startProximitySensor() {
409
299
  if (!proximityManager.isProximitySupported()) {
410
- // Log.d(TAG, "Proximity Sensor is not supported.");
300
+ RTKLogger.d("InCallManager", "Proximity Sensor is not supported.");
411
301
  return;
412
302
  }
413
303
  if (isProximityRegistered) {
414
- // Log.d(TAG, "Proximity Sensor is already registered.");
304
+ RTKLogger.d("InCallManager", "Proximity Sensor is already registered.");
415
305
  return;
416
306
  }
417
307
  // --- SENSOR_DELAY_FASTEST(0 milisecs), SENSOR_DELAY_GAME(20 milisecs), SENSOR_DELAY_UI(60 milisecs), SENSOR_DELAY_NORMAL(200 milisecs)
418
308
  if (!proximityManager.start()) {
419
- // Log.d(TAG, "proximityManager.start() failed. return false");
309
+ RTKLogger.d("InCallManager", "proximityManager.start() failed. return false");
420
310
  return;
421
311
  }
422
- // Log.d(TAG, "startProximitySensor()");
312
+ RTKLogger.d("InCallManager", "startProximitySensor()");
423
313
  isProximityRegistered = true;
424
314
  }
425
315
 
426
316
  @ReactMethod
427
317
  public void stopProximitySensor() {
428
318
  if (!proximityManager.isProximitySupported()) {
429
- // Log.d(TAG, "Proximity Sensor is not supported.");
319
+ RTKLogger.d("InCallManager", "Proximity Sensor is not supported.");
430
320
  return;
431
321
  }
432
322
  if (!isProximityRegistered) {
433
- // Log.d(TAG, "Proximity Sensor is not registered.");
323
+ RTKLogger.d("InCallManager", "Proximity Sensor is not registered.");
434
324
  return;
435
325
  }
436
- // Log.d(TAG, "stopProximitySensor()");
326
+ RTKLogger.d("InCallManager", "stopProximitySensor()");
437
327
  proximityManager.stop();
438
328
  isProximityRegistered = false;
439
329
  }
@@ -472,7 +362,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
472
362
  break;
473
363
  }
474
364
 
475
- // Log.d(TAG, "onAudioFocusChange(): " + focusChange + " - " + focusChangeStr);
365
+ RTKLogger.d("InCallManager", "onAudioFocusChange(): " + focusChange + " - " + focusChangeStr);
476
366
 
477
367
  WritableMap data = Arguments.createMap();
478
368
  data.putString("eventText", focusChangeStr);
@@ -484,36 +374,29 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
484
374
  try {
485
375
  ReactContext reactContext = getReactApplicationContext();
486
376
  if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
487
- // Log.i(TAG, "Sending event");
377
+ RTKLogger.d("InCallManager", "Sending event: " + eventName);
488
378
  reactContext
489
379
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
490
380
  .emit(eventName, params);
491
381
  } else {
492
- Log.e(TAG, "sendEvent(): reactContext is null or not having CatalystInstance yet.");
382
+ RTKLogger.e("InCallManager", "sendEvent(): reactContext is null or not having CatalystInstance yet.");
493
383
  }
494
384
  } catch (RuntimeException e) {
495
- Log.e(TAG, "sendEvent(): java.lang.RuntimeException: Trying to invoke JS before CatalystInstance has been set!");
385
+ RTKLogger.e("InCallManager", "sendEvent(): java.lang.RuntimeException: Trying to invoke JS before CatalystInstance has been set!");
496
386
  }
497
387
  }
498
388
 
389
+ /**
390
+ * Starts the audio manager. Default mode is video call (speaker on).
391
+ * Use setVoiceCallMode() to switch to voice call mode (earpiece with proximity sensor).
392
+ */
499
393
  @ReactMethod
500
- public void start(final String _media, final boolean auto, final String ringbackUriType) {
501
- media = _media;
502
- if (media.equals("video")) {
503
- defaultSpeakerOn = true;
504
- } else {
505
- defaultSpeakerOn = false;
506
- }
507
- automatic = auto;
394
+ public void start() {
508
395
  if (!audioManagerActivated) {
509
396
  audioManagerActivated = true;
510
397
 
511
- // Log.d(TAG, "start audioRouteManager");
398
+ RTKLogger.d("InCallManager", "start audioRouteManager");
512
399
  wakeLockUtils.acquirePartialWakeLock();
513
- if (mRingtone != null && mRingtone.isPlaying()) {
514
- // Log.d(TAG, "stop ringtone");
515
- stopRingtone(); // --- use brandnew instance
516
- }
517
400
  storeOriginalAudioSetup();
518
401
  requestAudioFocus();
519
402
  startEvents();
@@ -530,47 +413,93 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
530
413
  selectedAudioDevice = AudioDevice.NONE;
531
414
  audioDevices.clear();
532
415
  updateAudioRoute();
533
-
534
- if (!ringbackUriType.isEmpty()) {
535
- startRingback(ringbackUriType);
536
- }
537
416
  }
538
417
  }
539
418
 
419
+ /**
420
+ * Stops the audio manager and releases all resources.
421
+ */
422
+ @ReactMethod
540
423
  public void stop() {
541
- stop("");
424
+ if (audioManagerActivated) {
425
+ RTKLogger.d("InCallManager", "stop() InCallManager");
426
+ stopEvents();
427
+ setSpeakerphoneOn(false);
428
+ setMicrophoneMute(false);
429
+ forceSpeakerOn = 0;
430
+ UiThreadUtil.runOnUiThread(() -> {
431
+ bluetoothManager.stop();
432
+ });
433
+ restoreOriginalAudioSetup();
434
+ abandonAudioFocus();
435
+ audioManagerActivated = false;
436
+ wakeLockUtils.releasePartialWakeLock();
437
+ }
542
438
  }
543
439
 
440
+ /**
441
+ * Switches to voice call mode (earpiece with proximity sensor enabled).
442
+ * Call this after start() to enable proximity sensor for voice calls.
443
+ */
544
444
  @ReactMethod
545
- public void stop(final String busytoneUriType) {
546
- if (audioManagerActivated) {
547
- stopRingback();
548
- if (!busytoneUriType.isEmpty() && startBusytone(busytoneUriType)) {
549
- // play busytone first, and call this func again when finish
550
- // Log.d(TAG, "play busytone before stop InCallManager");
551
- return;
552
- } else {
553
- // Log.d(TAG, "stop() InCallManager");
554
- stopBusytone();
555
- stopEvents();
556
- setSpeakerphoneOn(false);
557
- setMicrophoneMute(false);
558
- forceSpeakerOn = 0;
559
- UiThreadUtil.runOnUiThread(() -> {
560
- bluetoothManager.stop();
561
- });
562
- restoreOriginalAudioSetup();
563
- abandonAudioFocus();
564
- audioManagerActivated = false;
445
+ public void setVoiceCallMode() {
446
+ defaultSpeakerOn = false;
447
+ defaultAudioDevice = hasEarpiece() ? AudioDevice.EARPIECE : AudioDevice.SPEAKER_PHONE;
448
+ userSelectedAudioDevice = AudioDevice.NONE;
449
+ updateAudioRoute();
450
+ }
451
+
452
+ /**
453
+ * Get all available audio output devices.
454
+ * Returns array of device names: ["SPEAKER_PHONE", "BLUETOOTH", "WIRED_HEADSET", "EARPIECE"]
455
+ */
456
+ @ReactMethod
457
+ public void getAudioDevices(Promise promise) {
458
+ try {
459
+ WritableMap result = Arguments.createMap();
460
+ String devicesJson = "[";
461
+ for (AudioDevice device : audioDevices) {
462
+ devicesJson += "\"" + device.name() + "\",";
565
463
  }
566
- wakeLockUtils.releasePartialWakeLock();
464
+ // Strip the last comma
465
+ if (devicesJson.length() > 1) {
466
+ devicesJson = devicesJson.substring(0, devicesJson.length() - 1);
467
+ }
468
+ devicesJson += "]";
469
+
470
+ result.putString("availableDevices", devicesJson);
471
+ promise.resolve(result);
472
+ } catch (Exception e) {
473
+ promise.reject("ERROR", "Failed to get audio devices", e);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Get the currently active audio output device.
479
+ * Returns device name: "SPEAKER_PHONE", "BLUETOOTH", "WIRED_HEADSET", "EARPIECE", or "NONE"
480
+ */
481
+ @ReactMethod
482
+ public void getCurrentAudioDevice(Promise promise) {
483
+ try {
484
+ WritableMap result = Arguments.createMap();
485
+ result.putString("currentDevice", selectedAudioDevice != null ? selectedAudioDevice.name() : "NONE");
486
+
487
+ // Also include Bluetooth SCO state for better debugging
488
+ if (bluetoothManager != null) {
489
+ result.putString("bluetoothState", bluetoothManager.getState().toString());
490
+ result.putBoolean("bluetoothScoConnected",
491
+ bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED);
492
+ }
493
+
494
+ promise.resolve(result);
495
+ } catch (Exception e) {
496
+ promise.reject("ERROR", "Failed to get current audio device", e);
567
497
  }
568
498
  }
569
499
 
570
500
  private void startEvents() {
571
501
  startWiredHeadsetEvent();
572
502
  startNoisyAudioEvent();
573
- startMediaButtonEvent();
574
503
  startProximitySensor(); // --- proximity event always enable, but only turn screen off when audio is routing to earpiece.
575
504
  setKeepScreenOn(true);
576
505
  }
@@ -578,7 +507,6 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
578
507
  private void stopEvents() {
579
508
  stopWiredHeadsetEvent();
580
509
  stopNoisyAudioEvent();
581
- stopMediaButtonEvent();
582
510
  stopProximitySensor();
583
511
  setKeepScreenOn(false);
584
512
  turnScreenOn();
@@ -593,7 +521,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
593
521
  String requestAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
594
522
  ? requestAudioFocusV26()
595
523
  : requestAudioFocusOld();
596
- // Log.d(TAG, "requestAudioFocus(): res = " + requestAudioFocusResStr);
524
+ RTKLogger.d("InCallManager", "requestAudioFocus(): res = " + requestAudioFocusResStr);
597
525
  return requestAudioFocusResStr;
598
526
  }
599
527
 
@@ -673,7 +601,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
673
601
  String abandonAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
674
602
  ? abandonAudioFocusV26()
675
603
  : abandonAudioFocusOld();
676
- // Log.d(TAG, "abandonAudioFocus(): res = " + abandonAudioFocusResStr);
604
+ RTKLogger.d("InCallManager", "abandonAudioFocus(): res = " + abandonAudioFocusResStr);
677
605
  return abandonAudioFocusResStr;
678
606
  }
679
607
 
@@ -726,7 +654,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
726
654
 
727
655
  @ReactMethod
728
656
  public void pokeScreen(int timeout) {
729
- // Log.d(TAG, "pokeScreen()");
657
+ RTKLogger.d("InCallManager", "pokeScreen()");
730
658
  wakeLockUtils.acquirePokeFullWakeLockReleaseAfter(timeout); // --- default 3000 ms
731
659
  }
732
660
 
@@ -766,16 +694,16 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
766
694
  } else {
767
695
  isInteractive = String.format("%s", mPowerManager.isScreenOn());
768
696
  }
769
- // Log.d(TAG, String.format("debugScreenPowerState(): screenState='%s', isInteractive='%s', isPowerSaveMode='%s', isDeviceIdleMode='%s', isIgnoringBatteryOptimizations='%s'", screenState, isInteractive, isPowerSaveMode, isDeviceIdleMode, isIgnoringBatteryOptimizations));
697
+ RTKLogger.d("InCallManager", String.format("debugScreenPowerState(): screenState='%s', isInteractive='%s', isPowerSaveMode='%s', isDeviceIdleMode='%s', isIgnoringBatteryOptimizations='%s'", screenState, isInteractive, isPowerSaveMode, isDeviceIdleMode, isIgnoringBatteryOptimizations));
770
698
  }
771
699
 
772
700
  @ReactMethod
773
701
  public void turnScreenOn() {
774
702
  if (proximityManager.isProximityWakeLockSupported()) {
775
- // Log.d(TAG, "turnScreenOn(): use proximity lock.");
703
+ RTKLogger.d("InCallManager", "turnScreenOn(): use proximity lock.");
776
704
  proximityManager.releaseProximityWakeLock(true);
777
705
  } else {
778
- // Log.d(TAG, "turnScreenOn(): proximity lock is not supported. try manually.");
706
+ RTKLogger.d("InCallManager", "turnScreenOn(): proximity lock is not supported. try manually.");
779
707
  manualTurnScreenOn();
780
708
  }
781
709
  }
@@ -783,22 +711,22 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
783
711
  @ReactMethod
784
712
  public void turnScreenOff() {
785
713
  if (proximityManager.isProximityWakeLockSupported()) {
786
- // Log.d(TAG, "turnScreenOff(): use proximity lock.");
714
+ RTKLogger.d("InCallManager", "turnScreenOff(): use proximity lock.");
787
715
  proximityManager.acquireProximityWakeLock();
788
716
  } else {
789
- // Log.d(TAG, "turnScreenOff(): proximity lock is not supported. try manually.");
717
+ RTKLogger.d("InCallManager", "turnScreenOff(): proximity lock is not supported. try manually.");
790
718
  manualTurnScreenOff();
791
719
  }
792
720
  }
793
721
 
794
722
  @ReactMethod
795
723
  public void setKeepScreenOn(final boolean enable) {
796
- // Log.d(TAG, "setKeepScreenOn() " + enable);
724
+ RTKLogger.d("InCallManager", "setKeepScreenOn() " + enable);
797
725
  UiThreadUtil.runOnUiThread(new Runnable() {
798
726
  public void run() {
799
727
  Activity mCurrentActivity = getCurrentActivity();
800
728
  if (mCurrentActivity == null) {
801
- // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
729
+ RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
802
730
  return;
803
731
  }
804
732
  Window window = mCurrentActivity.getWindow();
@@ -814,7 +742,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
814
742
  @ReactMethod
815
743
  public void setSpeakerphoneOn(final boolean enable) {
816
744
  if (enable != audioManager.isSpeakerphoneOn()) {
817
- // Log.d(TAG, "setSpeakerphoneOn(): " + enable);
745
+ RTKLogger.d("InCallManager", "setSpeakerphoneOn(): " + enable);
818
746
  audioManager.setSpeakerphoneOn(enable);
819
747
  }
820
748
  }
@@ -831,7 +759,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
831
759
  if (flag < -1 || flag > 1) {
832
760
  return;
833
761
  }
834
- // Log.d(TAG, "setForceSpeakerphoneOn() flag: " + flag);
762
+ RTKLogger.d("InCallManager", "setForceSpeakerphoneOn() flag: " + flag);
835
763
  forceSpeakerOn = flag;
836
764
 
837
765
  // --- will call updateAudioDeviceState()
@@ -850,622 +778,14 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
850
778
  @ReactMethod
851
779
  public void setMicrophoneMute(final boolean enable) {
852
780
  if (enable != audioManager.isMicrophoneMute()) {
853
- // Log.d(TAG, "setMicrophoneMute(): " + enable);
781
+ RTKLogger.d("InCallManager", "setMicrophoneMute(): " + enable);
854
782
  audioManager.setMicrophoneMute(enable);
855
783
  }
856
784
  }
857
785
 
858
- /**
859
- * This is part of start() process.
860
- * ringbackUriType must not empty. empty means do not play.
861
- */
862
- @ReactMethod
863
- public void startRingback(final String ringbackUriType) {
864
- if (ringbackUriType.isEmpty()) {
865
- return;
866
- }
867
- try {
868
- // Log.d(TAG, "startRingback(): UriType=" + ringbackUriType);
869
-
870
- if (mRingback != null) {
871
- if (mRingback.isPlaying()) {
872
- // Log.d(TAG, "startRingback(): is already playing");
873
- return;
874
- }
875
-
876
- stopRingback(); // --- use brandnew instance
877
- }
878
-
879
- Uri ringbackUri;
880
- Map data = new HashMap<String, Object>();
881
- data.put("name", "mRingback");
882
-
883
- // --- use ToneGenerator instead file uri
884
- if (ringbackUriType.equals("_DTMF_")) {
885
- mRingback = new myToneGenerator(myToneGenerator.RINGBACK);
886
- mRingback.startPlay(data);
887
- return;
888
- }
889
-
890
- ringbackUri = getRingbackUri(ringbackUriType);
891
- if (ringbackUri == null) {
892
- // Log.d(TAG, "startRingback(): no available media");
893
- return;
894
- }
895
-
896
- mRingback = new myMediaPlayer();
897
- data.put("sourceUri", ringbackUri);
898
- data.put("setLooping", true);
899
-
900
- //data.put("audioStream", AudioManager.STREAM_VOICE_CALL); // --- lagacy
901
- // --- The ringback doesn't have to be a DTMF.
902
- // --- Should use VOICE_COMMUNICATION for sound during call or it may be silenced.
903
- data.put("audioUsage", AudioAttributes.USAGE_VOICE_COMMUNICATION);
904
- data.put("audioContentType", AudioAttributes.CONTENT_TYPE_MUSIC);
905
-
906
- setMediaPlayerEvents((MediaPlayer)mRingback, "mRingback");
907
-
908
- mRingback.startPlay(data);
909
- } catch(Exception e) {
910
- // Log.d(TAG, "startRingback() failed", e);
911
- }
912
- }
913
-
914
- @ReactMethod
915
- public void stopRingback() {
916
- try {
917
- if (mRingback != null) {
918
- mRingback.stopPlay();
919
- mRingback = null;
920
- }
921
- } catch(Exception e) {
922
- // Log.d(TAG, "stopRingback() failed");
923
- }
924
- }
925
-
926
- /**
927
- * This is part of start() process.
928
- * busytoneUriType must not empty. empty means do not play.
929
- * return false to indicate play tone failed and should be stop() immediately
930
- * otherwise, it will stop() after a tone completed.
931
- */
932
- public boolean startBusytone(final String busytoneUriType) {
933
- if (busytoneUriType.isEmpty()) {
934
- return false;
935
- }
936
- try {
937
- // Log.d(TAG, "startBusytone(): UriType=" + busytoneUriType);
938
- if (mBusytone != null) {
939
- if (mBusytone.isPlaying()) {
940
- // Log.d(TAG, "startBusytone(): is already playing");
941
- return false;
942
- }
943
-
944
- stopBusytone(); // --- use brandnew instance
945
- }
946
-
947
- Uri busytoneUri;
948
- Map data = new HashMap<String, Object>();
949
- data.put("name", "mBusytone");
950
-
951
- // --- use ToneGenerator instead file uri
952
- if (busytoneUriType.equals("_DTMF_")) {
953
- mBusytone = new myToneGenerator(myToneGenerator.BUSY);
954
- mBusytone.startPlay(data);
955
- return true;
956
- }
957
-
958
- busytoneUri = getBusytoneUri(busytoneUriType);
959
- if (busytoneUri == null) {
960
- // Log.d(TAG, "startBusytone(): no available media");
961
- return false;
962
- }
963
-
964
- mBusytone = new myMediaPlayer();
965
-
966
- data.put("sourceUri", busytoneUri);
967
- data.put("setLooping", false);
968
- //data.put("audioStream", AudioManager.STREAM_VOICE_CALL); // --- lagacy
969
- // --- Should use VOICE_COMMUNICATION for sound during a call or it may be silenced.
970
- data.put("audioUsage", AudioAttributes.USAGE_VOICE_COMMUNICATION);
971
- data.put("audioContentType", AudioAttributes.CONTENT_TYPE_SONIFICATION); // --- CONTENT_TYPE_MUSIC?
972
-
973
- setMediaPlayerEvents((MediaPlayer)mBusytone, "mBusytone");
974
- mBusytone.startPlay(data);
975
- return true;
976
- } catch(Exception e) {
977
- // Log.d(TAG, "startBusytone() failed", e);
978
- return false;
979
- }
980
- }
981
-
982
- public void stopBusytone() {
983
- try {
984
- if (mBusytone != null) {
985
- mBusytone.stopPlay();
986
- mBusytone = null;
987
- }
988
- } catch(Exception e) {
989
- // Log.d(TAG, "stopBusytone() failed");
990
- }
991
- }
992
-
993
- @ReactMethod
994
- public void startRingtone(final String ringtoneUriType, final int seconds) {
995
- Thread thread = new Thread() {
996
- @Override
997
- public void run() {
998
- try {
999
- Looper.prepare();
1000
-
1001
- // Log.d(TAG, "startRingtone(): UriType=" + ringtoneUriType);
1002
- if (mRingtone != null) {
1003
- if (mRingtone.isPlaying()) {
1004
- // Log.d(TAG, "startRingtone(): is already playing");
1005
- return;
1006
- } else {
1007
- stopRingtone(); // --- use brandnew instance
1008
- }
1009
- }
1010
-
1011
- //if (!audioManager.isStreamMute(AudioManager.STREAM_RING)) {
1012
- //if (origRingerMode == AudioManager.RINGER_MODE_NORMAL) {
1013
- if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
1014
- // Log.d(TAG, "startRingtone(): ringer is silent. leave without play.");
1015
- return;
1016
- }
1017
-
1018
- // --- there is no _DTMF_ option in startRingtone()
1019
- Uri ringtoneUri = getRingtoneUri(ringtoneUriType);
1020
- if (ringtoneUri == null) {
1021
- // Log.d(TAG, "startRingtone(): no available media");
1022
- return;
1023
- }
1024
-
1025
- if (audioManagerActivated) {
1026
- InCallManagerModule.this.stop();
1027
- }
1028
-
1029
- wakeLockUtils.acquirePartialWakeLock();
1030
-
1031
- storeOriginalAudioSetup();
1032
- Map data = new HashMap<String, Object>();
1033
- mRingtone = new myMediaPlayer();
1034
-
1035
- data.put("name", "mRingtone");
1036
- data.put("sourceUri", ringtoneUri);
1037
- data.put("setLooping", true);
1038
-
1039
- //data.put("audioStream", AudioManager.STREAM_RING); // --- lagacy
1040
- data.put("audioUsage", AudioAttributes.USAGE_NOTIFICATION_RINGTONE); // --- USAGE_NOTIFICATION_COMMUNICATION_REQUEST?
1041
- data.put("audioContentType", AudioAttributes.CONTENT_TYPE_MUSIC);
1042
-
1043
- setMediaPlayerEvents((MediaPlayer) mRingtone, "mRingtone");
1044
-
1045
- mRingtone.startPlay(data);
1046
-
1047
- if (seconds > 0) {
1048
- mRingtoneCountDownHandler = new Handler();
1049
- mRingtoneCountDownHandler.postDelayed(new Runnable() {
1050
- public void run() {
1051
- try {
1052
- // Log.d(TAG, String.format("mRingtoneCountDownHandler.stopRingtone() timeout after %d seconds", seconds));
1053
- stopRingtone();
1054
- } catch(Exception e) {
1055
- // Log.d(TAG, "mRingtoneCountDownHandler.stopRingtone() failed.");
1056
- }
1057
- }
1058
- }, seconds * 1000);
1059
- }
1060
-
1061
- Looper.loop();
1062
- } catch(Exception e) {
1063
- wakeLockUtils.releasePartialWakeLock();
1064
- // Log.e(TAG, "startRingtone() failed", e);
1065
- }
1066
- }
1067
- };
1068
-
1069
- thread.start();
1070
- }
1071
-
1072
- @ReactMethod
1073
- public void stopRingtone() {
1074
- Thread thread = new Thread() {
1075
- @Override
1076
- public void run() {
1077
- try {
1078
- if (mRingtone != null) {
1079
- mRingtone.stopPlay();
1080
- mRingtone = null;
1081
- restoreOriginalAudioSetup();
1082
- }
1083
- if (mRingtoneCountDownHandler != null) {
1084
- mRingtoneCountDownHandler.removeCallbacksAndMessages(null);
1085
- mRingtoneCountDownHandler = null;
1086
- }
1087
- } catch (Exception e) {
1088
- // Log.d(TAG, "stopRingtone() failed");
1089
- }
1090
- wakeLockUtils.releasePartialWakeLock();
1091
- }
1092
- };
1093
-
1094
- thread.start();
1095
- }
1096
-
1097
- private void setMediaPlayerEvents(MediaPlayer mp, final String name) {
1098
-
1099
- mp.setOnErrorListener(new MediaPlayer.OnErrorListener() {
1100
- //http://developer.android.com/reference/android/media/MediaPlayer.OnErrorListener.html
1101
- @Override
1102
- public boolean onError(MediaPlayer mp, int what, int extra) {
1103
- // Log.d(TAG, String.format("MediaPlayer %s onError(). what: %d, extra: %d", name, what, extra));
1104
- //return True if the method handled the error
1105
- //return False, or not having an OnErrorListener at all, will cause the OnCompletionListener to be called. Get news & tips
1106
- return true;
1107
- }
1108
- });
1109
-
1110
- mp.setOnInfoListener(new MediaPlayer.OnInfoListener() {
1111
- //http://developer.android.com/reference/android/media/MediaPlayer.OnInfoListener.html
1112
- @Override
1113
- public boolean onInfo(MediaPlayer mp, int what, int extra) {
1114
- // Log.d(TAG, String.format("MediaPlayer %s onInfo(). what: %d, extra: %d", name, what, extra));
1115
- //return True if the method handled the info
1116
- //return False, or not having an OnInfoListener at all, will cause the info to be discarded.
1117
- return true;
1118
- }
1119
- });
1120
-
1121
- mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
1122
- @Override
1123
- public void onPrepared(MediaPlayer mp) {
1124
- // Log.d(TAG, String.format("MediaPlayer %s onPrepared(), start play, isSpeakerPhoneOn %b", name, audioManager.isSpeakerphoneOn()));
1125
- if (name.equals("mBusytone")) {
1126
- audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1127
- } else if (name.equals("mRingback")) {
1128
- audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1129
- } else if (name.equals("mRingtone")) {
1130
- audioManager.setMode(AudioManager.MODE_RINGTONE);
1131
- }
1132
- updateAudioRoute();
1133
- mp.start();
1134
- }
1135
- });
1136
-
1137
- mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
1138
- @Override
1139
- public void onCompletion(MediaPlayer mp) {
1140
- // Log.d(TAG, String.format("MediaPlayer %s onCompletion()", name));
1141
- if (name.equals("mBusytone")) {
1142
- // Log.d(TAG, "MyMediaPlayer(): invoke stop()");
1143
- stop();
1144
- }
1145
- }
1146
- });
1147
-
1148
- }
1149
-
1150
-
1151
- // ===== File Uri Start =====
1152
- @ReactMethod
1153
- public void getAudioUriJS(String audioType, String fileType, Promise promise) {
1154
- Uri result = null;
1155
- if (audioType.equals("ringback")) {
1156
- result = getRingbackUri(fileType);
1157
- } else if (audioType.equals("busytone")) {
1158
- result = getBusytoneUri(fileType);
1159
- } else if (audioType.equals("ringtone")) {
1160
- result = getRingtoneUri(fileType);
1161
- }
1162
- try {
1163
- if (result != null) {
1164
- promise.resolve(result.toString());
1165
- } else {
1166
- promise.reject("failed");
1167
- }
1168
- } catch (Exception e) {
1169
- promise.reject("failed");
1170
- }
1171
- }
1172
-
1173
- private Uri getRingtoneUri(final String _type) {
1174
- final String fileBundle = "incallmanager_ringtone";
1175
- final String fileBundleExt = "mp3";
1176
- final String fileSysWithExt = "media_volume.ogg";
1177
- final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock"
1178
- String type;
1179
- // --- _type MAY be empty
1180
- if (_type.equals("_DEFAULT_") || _type.isEmpty()) {
1181
- //type = fileSysWithExt;
1182
- return getDefaultUserUri("defaultRingtoneUri");
1183
- } else {
1184
- type = _type;
1185
- }
1186
- return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleRingtoneUri", "defaultRingtoneUri");
1187
- }
1188
-
1189
- private Uri getRingbackUri(final String _type) {
1190
- final String fileBundle = "incallmanager_ringback";
1191
- final String fileBundleExt = "mp3";
1192
- final String fileSysWithExt = "media_volume.ogg";
1193
- final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock"
1194
- String type;
1195
- // --- _type would never be empty here. just in case.
1196
- if (_type.equals("_DEFAULT_") || _type.isEmpty()) {
1197
- //type = fileSysWithExt;
1198
- return getDefaultUserUri("defaultRingbackUri");
1199
- } else {
1200
- type = _type;
1201
- }
1202
- return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleRingbackUri", "defaultRingbackUri");
1203
- }
1204
-
1205
- private Uri getBusytoneUri(final String _type) {
1206
- final String fileBundle = "incallmanager_busytone";
1207
- final String fileBundleExt = "mp3";
1208
- final String fileSysWithExt = "LowBattery.ogg";
1209
- final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock"
1210
- String type;
1211
- // --- _type would never be empty here. just in case.
1212
- if (_type.equals("_DEFAULT_") || _type.isEmpty()) {
1213
- //type = fileSysWithExt; // ---
1214
- return getDefaultUserUri("defaultBusytoneUri");
1215
- } else {
1216
- type = _type;
1217
- }
1218
- return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleBusytoneUri", "defaultBusytoneUri");
1219
- }
1220
-
1221
- private Uri getAudioUri(final String _type, final String fileBundle, final String fileBundleExt, final String fileSysWithExt, final String fileSysPath, final String uriBundle, final String uriDefault) {
1222
- String type = _type;
1223
- if (type.equals("_BUNDLE_")) {
1224
- if (audioUriMap.get(uriBundle) == null) {
1225
- int res = 0;
1226
- ReactContext reactContext = getReactApplicationContext();
1227
- if (reactContext != null) {
1228
- res = reactContext.getResources().getIdentifier(fileBundle, "raw", mPackageName);
1229
- } else {
1230
- // Log.d(TAG, "getAudioUri() reactContext is null");
1231
- }
1232
- if (res <= 0) {
1233
- // Log.d(TAG, String.format("getAudioUri() %s.%s not found in bundle.", fileBundle, fileBundleExt));
1234
- audioUriMap.put(uriBundle, null);
1235
- //type = fileSysWithExt;
1236
- return getDefaultUserUri(uriDefault); // --- if specified bundle but not found, use default directlly
1237
- } else {
1238
- audioUriMap.put(uriBundle, Uri.parse("android.resource://" + mPackageName + "/" + Integer.toString(res)));
1239
- //bundleRingtoneUri = Uri.parse("android.resource://" + reactContext.getPackageName() + "/" + R.raw.incallmanager_ringtone);
1240
- //bundleRingtoneUri = Uri.parse("android.resource://" + reactContext.getPackageName() + "/raw/incallmanager_ringtone");
1241
- // Log.d(TAG, "getAudioUri() using: " + type);
1242
- return audioUriMap.get(uriBundle);
1243
- }
1244
- } else {
1245
- // Log.d(TAG, "getAudioUri() using: " + type);
1246
- return audioUriMap.get(uriBundle);
1247
- }
1248
- }
1249
-
1250
- // --- Check file every time in case user deleted.
1251
- final String target = fileSysPath + "/" + type;
1252
- Uri _uri = getSysFileUri(target);
1253
- if (_uri == null) {
1254
- // Log.d(TAG, "getAudioUri() using user default");
1255
- return getDefaultUserUri(uriDefault);
1256
- } else {
1257
- // Log.d(TAG, "getAudioUri() using internal: " + target);
1258
- audioUriMap.put(uriDefault, _uri);
1259
- return _uri;
1260
- }
1261
- }
1262
-
1263
- private Uri getSysFileUri(final String target) {
1264
- File file = new File(target);
1265
- if (file.isFile()) {
1266
- return Uri.fromFile(file);
1267
- }
1268
- return null;
1269
- }
1270
-
1271
- private Uri getDefaultUserUri(final String type) {
1272
- // except ringtone, it doesn't suppose to be go here. and every android has different files unlike apple;
1273
- if (type.equals("defaultRingtoneUri")) {
1274
- return Settings.System.DEFAULT_RINGTONE_URI;
1275
- } else if (type.equals("defaultRingbackUri")) {
1276
- return Settings.System.DEFAULT_RINGTONE_URI;
1277
- } else if (type.equals("defaultBusytoneUri")) {
1278
- return Settings.System.DEFAULT_NOTIFICATION_URI; // --- DEFAULT_ALARM_ALERT_URI
1279
- } else {
1280
- return Settings.System.DEFAULT_NOTIFICATION_URI;
1281
- }
1282
- }
1283
- // ===== File Uri End =====
1284
-
1285
-
1286
- // ===== Internal Classes Start =====
1287
- private class myToneGenerator extends Thread implements MyPlayerInterface {
1288
- private int toneType;
1289
- private int toneCategory;
1290
- private boolean playing = false;
1291
- private static final int maxWaitTimeMs = 3600000; // 1 hour fairly enough
1292
- private static final int loadBufferWaitTimeMs = 20;
1293
- private static final int toneVolume = 100; // The volume of the tone, given in percentage of maximum volume (from 0-100).
1294
- // --- constant in ToneGenerator all below 100
1295
- public static final int BEEP = 101;
1296
- public static final int BUSY = 102;
1297
- public static final int CALLEND = 103;
1298
- public static final int CALLWAITING = 104;
1299
- public static final int RINGBACK = 105;
1300
- public static final int SILENT = 106;
1301
- public int customWaitTimeMs = maxWaitTimeMs;
1302
- public String caller;
1303
-
1304
- myToneGenerator(final int t) {
1305
- super();
1306
- toneCategory = t;
1307
- }
1308
-
1309
- public void setCustomWaitTime(final int ms) {
1310
- customWaitTimeMs = ms;
1311
- }
1312
-
1313
- @Override
1314
- public void startPlay(final Map data) {
1315
- String name = (String) data.get("name");
1316
- caller = name;
1317
- start();
1318
- }
1319
-
1320
- @Override
1321
- public boolean isPlaying() {
1322
- return playing;
1323
- }
1324
-
1325
- @Override
1326
- public void stopPlay() {
1327
- synchronized (this) {
1328
- if (playing) {
1329
- notify();
1330
- }
1331
- playing = false;
1332
- }
1333
- }
1334
-
1335
- @Override
1336
- public void run() {
1337
- int toneWaitTimeMs;
1338
- switch (toneCategory) {
1339
- case SILENT:
1340
- //toneType = ToneGenerator.TONE_CDMA_SIGNAL_OFF;
1341
- toneType = ToneGenerator.TONE_CDMA_ANSWER;
1342
- toneWaitTimeMs = 1000;
1343
- break;
1344
- case BUSY:
1345
- //toneType = ToneGenerator.TONE_SUP_BUSY;
1346
- //toneType = ToneGenerator.TONE_SUP_CONGESTION;
1347
- //toneType = ToneGenerator.TONE_SUP_CONGESTION_ABBREV;
1348
- //toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY;
1349
- //toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT;
1350
- toneType = ToneGenerator.TONE_SUP_RADIO_NOTAVAIL;
1351
- toneWaitTimeMs = 4000;
1352
- break;
1353
- case RINGBACK:
1354
- //toneType = ToneGenerator.TONE_SUP_RINGTONE;
1355
- toneType = ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK;
1356
- toneWaitTimeMs = maxWaitTimeMs; // [STOP MANUALLY]
1357
- break;
1358
- case CALLEND:
1359
- toneType = ToneGenerator.TONE_PROP_PROMPT;
1360
- toneWaitTimeMs = 200; // plays when call ended
1361
- break;
1362
- case CALLWAITING:
1363
- //toneType = ToneGenerator.TONE_CDMA_NETWORK_CALLWAITING;
1364
- toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
1365
- toneWaitTimeMs = maxWaitTimeMs; // [STOP MANUALLY]
1366
- break;
1367
- case BEEP:
1368
- //toneType = ToneGenerator.TONE_SUP_PIP;
1369
- //toneType = ToneGenerator.TONE_CDMA_PIP;
1370
- //toneType = ToneGenerator.TONE_SUP_RADIO_ACK;
1371
- //toneType = ToneGenerator.TONE_PROP_BEEP;
1372
- toneType = ToneGenerator.TONE_PROP_BEEP2;
1373
- toneWaitTimeMs = 1000; // plays when call ended
1374
- break;
1375
- default:
1376
- // --- use ToneGenerator internal type.
1377
- // Log.d(TAG, "myToneGenerator: use internal tone type: " + toneCategory);
1378
- toneType = toneCategory;
1379
- toneWaitTimeMs = customWaitTimeMs;
1380
- }
1381
- // Log.d(TAG, String.format("myToneGenerator: toneCategory: %d ,toneType: %d, toneWaitTimeMs: %d", toneCategory, toneType, toneWaitTimeMs));
1382
-
1383
- ToneGenerator tg;
1384
- try {
1385
- tg = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, toneVolume);
1386
- } catch (RuntimeException e) {
1387
- // Log.d(TAG, "myToneGenerator: Exception caught while creating ToneGenerator: " + e);
1388
- tg = null;
1389
- }
1390
-
1391
- if (tg != null) {
1392
- synchronized (this) {
1393
- if (!playing) {
1394
- playing = true;
1395
-
1396
- // --- make sure audio routing, or it will be wired when switch suddenly
1397
- if (caller.equals("mBusytone")) {
1398
- audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1399
- } else if (caller.equals("mRingback")) {
1400
- audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1401
- } else if (caller.equals("mRingtone")) {
1402
- audioManager.setMode(AudioManager.MODE_RINGTONE);
1403
- }
1404
- InCallManagerModule.this.updateAudioRoute();
1405
-
1406
- tg.startTone(toneType);
1407
- try {
1408
- wait(toneWaitTimeMs + loadBufferWaitTimeMs);
1409
- } catch (InterruptedException e) {
1410
- // Log.d(TAG, "myToneGenerator stopped. toneType: " + toneType);
1411
- }
1412
- tg.stopTone();
1413
- }
1414
- playing = false;
1415
- tg.release();
1416
- }
1417
- }
1418
- // Log.d(TAG, "MyToneGenerator(): play finished. caller=" + caller);
1419
- if (caller.equals("mBusytone")) {
1420
- // Log.d(TAG, "MyToneGenerator(): invoke stop()");
1421
- InCallManagerModule.this.stop();
1422
- }
1423
- }
1424
- }
1425
-
1426
- private class myMediaPlayer extends MediaPlayer implements MyPlayerInterface {
1427
-
1428
- @Override
1429
- public void stopPlay() {
1430
- stop();
1431
- reset();
1432
- release();
1433
- }
1434
-
1435
- @Override
1436
- public void startPlay(final Map data) {
1437
- try {
1438
- ReactContext reactContext = getReactApplicationContext();
1439
-
1440
- setDataSource(reactContext, (Uri) data.get("sourceUri"));
1441
- setLooping((Boolean) data.get("setLooping"));
1442
-
1443
- // --- the `minSdkVersion` is 21 since RN 64,
1444
- // --- if you want to suuport api < 21, comment out `setAudioAttributes` and use `setAudioStreamType((Integer) data.get("audioStream"))` instead
1445
- setAudioAttributes(
1446
- new AudioAttributes.Builder()
1447
- .setUsage((Integer) data.get("audioUsage"))
1448
- .setContentType((Integer) data.get("audioContentType"))
1449
- .build()
1450
- );
1451
-
1452
- // -- will start at onPrepared() event
1453
- prepareAsync();
1454
- } catch (Exception e) {
1455
- // Log.d(TAG, "startPlay() failed", e);
1456
- }
1457
- }
1458
-
1459
- @Override
1460
- public boolean isPlaying() {
1461
- return super.isPlaying();
1462
- }
1463
- }
1464
- // ===== Internal Classes End =====
1465
-
1466
786
  @ReactMethod
1467
787
  public void chooseAudioRoute(String audioRoute, Promise promise) {
1468
- // Log.e(TAG, "RNInCallManager.chooseAudioRoute(): user choose audioDevice = " + audioRoute);
788
+ RTKLogger.d("InCallManager", "chooseAudioRoute(): user choose audioDevice = " + audioRoute);
1469
789
 
1470
790
  if (audioRoute.equals(AudioDevice.EARPIECE.name()) || audioRoute.equals("earpiece")) {
1471
791
  selectAudioDevice(AudioDevice.EARPIECE);
@@ -1479,46 +799,35 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1479
799
  promise.resolve(getAudioDeviceStatusMap());
1480
800
  }
1481
801
 
1482
- private static int getRandomInteger(int min, int max) {
1483
- if (min >= max) {
1484
- throw new IllegalArgumentException("max must be greater than min");
1485
- }
1486
- Random random = new Random();
1487
- return random.nextInt((max - min) + 1) + min;
1488
- }
1489
-
1490
802
  private void pause() {
1491
803
  if (audioManagerActivated) {
1492
- // Log.d(TAG, "pause audioRouteManager");
804
+ RTKLogger.d("InCallManager", "pause audioRouteManager");
1493
805
  stopEvents();
1494
806
  }
1495
807
  }
1496
808
 
1497
809
  private void resume() {
1498
810
  if (audioManagerActivated) {
1499
- // Log.d(TAG, "resume audioRouteManager");
811
+ RTKLogger.d("InCallManager", "resume audioRouteManager");
1500
812
  startEvents();
1501
813
  }
1502
814
  }
1503
815
 
1504
816
  @Override
1505
817
  public void onHostResume() {
1506
- // Log.d(TAG, "onResume()");
818
+ RTKLogger.d("InCallManager", "onResume()");
1507
819
  //resume();
1508
820
  }
1509
821
 
1510
822
  @Override
1511
823
  public void onHostPause() {
1512
- // Log.d(TAG, "onPause()");
824
+ RTKLogger.d("InCallManager", "onPause()");
1513
825
  //pause();
1514
826
  }
1515
827
 
1516
828
  @Override
1517
829
  public void onHostDestroy() {
1518
- // Log.d(TAG, "onDestroy()");
1519
- stopRingtone();
1520
- stopRingback();
1521
- stopBusytone();
830
+ RTKLogger.d("InCallManager", "onDestroy()");
1522
831
  stop();
1523
832
  }
1524
833
 
@@ -1532,9 +841,9 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1532
841
  // ===== NOTE: below functions is based on appRTC DEMO M64 ===== //
1533
842
  /** Changes selection of the currently active audio device. */
1534
843
  private void setAudioDeviceInternal(AudioDevice device) {
1535
- // Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")");
844
+ RTKLogger.d("InCallManager", "setAudioDeviceInternal(device=" + device + ")");
1536
845
  if (!audioDevices.contains(device)) {
1537
- // Log.e(TAG, "specified audio device does not exist");
846
+ RTKLogger.e("InCallManager", "specified audio device does not exist");
1538
847
  return;
1539
848
  }
1540
849
  switch (device) {
@@ -1544,10 +853,19 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1544
853
  stopProximitySensor();
1545
854
  break;
1546
855
  case EARPIECE:
856
+ RTKLogger.d("InCallManager", "Routing to EARPIECE");
1547
857
  audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1548
858
  setSpeakerphoneOn(false);
1549
- startProximitySensor();
1550
859
  audioManager.setWiredHeadsetOn(false);
860
+ // Ensure audio is routed to earpiece by stopping any Bluetooth SCO
861
+ if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
862
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) {
863
+ RTKLogger.d("InCallManager", "Stopping Bluetooth SCO for earpiece");
864
+ bluetoothManager.stopScoAudio();
865
+ }
866
+ startProximitySensor();
867
+ RTKLogger.d("InCallManager", "Earpiece routing complete. Mode: " + audioManager.getMode()
868
+ + ", Speaker: " + audioManager.isSpeakerphoneOn());
1551
869
  break;
1552
870
  case WIRED_HEADSET:
1553
871
  audioManager.setWiredHeadsetOn(true);
@@ -1560,7 +878,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1560
878
  stopProximitySensor();
1561
879
  break;
1562
880
  default:
1563
- // Log.e(TAG, "Invalid audio device selection");
881
+ RTKLogger.e("InCallManager", "Invalid audio device selection");
1564
882
  break;
1565
883
  }
1566
884
  selectedAudioDevice = device;
@@ -1585,20 +903,20 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1585
903
  }
1586
904
  break;
1587
905
  default:
1588
- // Log.e(TAG, "Invalid default audio device selection");
906
+ RTKLogger.e("InCallManager", "Invalid default audio device selection");
1589
907
  break;
1590
908
  }
1591
- // Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
909
+ RTKLogger.d("InCallManager", "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
1592
910
  updateAudioDeviceState();
1593
911
  }
1594
912
 
1595
913
  /** Changes selection of the currently active audio device. */
1596
914
  public void selectAudioDevice(AudioDevice device) {
1597
- // Log.e(TAG,"Selecting "+device.name());
915
+ RTKLogger.d("InCallManager", "Selecting " + device.name());
1598
916
  userSelectedAudioDevice = device;
1599
917
  updateAudioDeviceState();
1600
918
  if (device != AudioDevice.NONE && !audioDevices.contains(device)) {
1601
- // Log.e(TAG, "selectAudioDevice() Can not select " + device + " from available " + audioDevices);
919
+ RTKLogger.e("InCallManager", "selectAudioDevice() Can not select " + device + " from available " + audioDevices);
1602
920
  return;
1603
921
  }
1604
922
  }
@@ -1632,34 +950,12 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1632
950
  try {
1633
951
  reactContext.unregisterReceiver(receiver);
1634
952
  } catch (final Exception e) {
1635
- // Log.d(TAG, "unregisterReceiver() failed");
953
+ RTKLogger.d("InCallManager", "unregisterReceiver() failed");
1636
954
  }
1637
955
  } else {
1638
- // Log.d(TAG, "unregisterReceiver() reactContext is null");
1639
- }
1640
- }
1641
-
1642
- /** Sets the speaker phone mode. */
1643
- /*
1644
- private void setSpeakerphoneOn(boolean on) {
1645
- boolean wasOn = audioManager.isSpeakerphoneOn();
1646
- if (wasOn == on) {
1647
- return;
1648
- }
1649
- audioManager.setSpeakerphoneOn(on);
1650
- }
1651
- */
1652
-
1653
- /** Sets the microphone mute state. */
1654
- /*
1655
- private void setMicrophoneMute(boolean on) {
1656
- boolean wasMuted = audioManager.isMicrophoneMute();
1657
- if (wasMuted == on) {
1658
- return;
956
+ RTKLogger.d("InCallManager", "unregisterReceiver() reactContext is null");
1659
957
  }
1660
- audioManager.setMicrophoneMute(on);
1661
958
  }
1662
- */
1663
959
 
1664
960
  /** Gets the current earpiece state. */
1665
961
  private boolean hasEarpiece() {
@@ -1682,13 +978,13 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1682
978
  for (AudioDeviceInfo device : devices) {
1683
979
  final int type = device.getType();
1684
980
  if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
1685
- // Log.d(TAG, "hasWiredHeadset: found wired headset");
981
+ RTKLogger.d("InCallManager", "hasWiredHeadset: found wired headset");
1686
982
  return true;
1687
983
  } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
1688
- // Log.d(TAG, "hasWiredHeadset: found USB audio device");
984
+ RTKLogger.d("InCallManager", "hasWiredHeadset: found USB audio device");
1689
985
  return true;
1690
986
  } else if (type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
1691
- // Log.d(TAG, "hasWiredHeadset: found wired headphones");
987
+ RTKLogger.d("InCallManager", "hasWiredHeadset: found wired headphones");
1692
988
  return true;
1693
989
  }
1694
990
  }
@@ -1709,14 +1005,14 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1709
1005
  || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
1710
1006
  || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
1711
1007
  promise.resolve(true);
1712
- // Log.i("Bluetooth State:", "isBluetoothHeadsetConnected: true");
1008
+ RTKLogger.i("InCallManager", "isBluetoothHeadsetConnected: true");
1713
1009
  } else {
1714
1010
  promise.resolve(false);
1715
- // Log.i("Bluetooth State:", "isBluetoothHeadsetConnected: false");
1011
+ RTKLogger.i("InCallManager", "isBluetoothHeadsetConnected: false");
1716
1012
  }
1717
1013
  } catch (Exception e) {
1718
1014
  promise.resolve(false);
1719
- Log.e("Bluetooth State:", "isBluetoothHeadsetConnected: false due to: ", e);
1015
+ RTKLogger.e("InCallManager", "isBluetoothHeadsetConnected: false due to: " + e.getMessage(), e);
1720
1016
  }
1721
1017
  });
1722
1018
  }
@@ -1727,17 +1023,17 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1727
1023
  */
1728
1024
  public void updateAudioDeviceState() {
1729
1025
  UiThreadUtil.runOnUiThread(() -> {
1730
- // Log.e("Bluetooth State: ",bluetoothManager.getState().toString());
1026
+ RTKLogger.d("InCallManager", "Bluetooth State: " + bluetoothManager.getState().toString());
1731
1027
  if(bluetoothManager.getState().toString().equals("UNINITIALIZED"))
1732
1028
  bluetoothManager.start();
1733
1029
  hasWiredHeadset= this.hasWiredHeadset();
1734
- // Log.d(TAG, "--- updateAudioDeviceState: "
1735
- // + "wired headset=" + hasWiredHeadset + ", "
1736
- // + "BT state=" + bluetoothManager.getState());
1737
- // Log.d(TAG, "Device status: "
1738
- // + "available=" + audioDevices + ", "
1739
- // + "selected=" + selectedAudioDevice + ", "
1740
- // + "user selected=" + userSelectedAudioDevice);
1030
+ RTKLogger.d("InCallManager", "--- updateAudioDeviceState: "
1031
+ + "wired headset=" + hasWiredHeadset + ", "
1032
+ + "BT state=" + bluetoothManager.getState());
1033
+ RTKLogger.d("InCallManager", "Device status: "
1034
+ + "available=" + audioDevices + ", "
1035
+ + "selected=" + selectedAudioDevice + ", "
1036
+ + "user selected=" + userSelectedAudioDevice);
1741
1037
 
1742
1038
  // Check if any Bluetooth headset is connected. The internal BT state will
1743
1039
  // change accordingly.
@@ -1792,23 +1088,30 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1792
1088
  }
1793
1089
 
1794
1090
  // --- start bluetooth if needed
1795
- if (selectedAudioDevice != AudioDevice.BLUETOOTH
1796
- && newAudioDevice == AudioDevice.BLUETOOTH
1091
+ // Start SCO if: switching to Bluetooth OR already on Bluetooth but SCO not connected yet
1092
+ if (newAudioDevice == AudioDevice.BLUETOOTH
1797
1093
  && bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
1798
1094
  // Attempt to start Bluetooth SCO audio (takes a few second to start).
1095
+ RTKLogger.d("InCallManager", "Starting Bluetooth SCO audio...");
1799
1096
  if (!bluetoothManager.startScoAudio()) {
1800
1097
  // Remove BLUETOOTH from list of available devices since SCO failed.
1098
+ RTKLogger.e("InCallManager", "Failed to start Bluetooth SCO audio");
1801
1099
  audioDevices.remove(AudioDevice.BLUETOOTH);
1802
1100
  audioDeviceSetUpdated = true;
1803
1101
  if (userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
1804
1102
  userSelectedAudioDevice = AudioDevice.NONE;
1805
1103
  }
1806
1104
  newAudioDevice = getPreferredAudioDevice();
1105
+ } else {
1106
+ RTKLogger.d("InCallManager", "Bluetooth SCO audio start initiated");
1807
1107
  }
1808
1108
  }
1809
1109
 
1110
+ // Only fall back from Bluetooth if user didn't explicitly select it
1111
+ // If user selected Bluetooth, wait for SCO connection instead of falling back
1810
1112
  if (newAudioDevice == AudioDevice.BLUETOOTH
1811
- && bluetoothManager.getState() != AppRTCBluetoothManager.State.SCO_CONNECTED) {
1113
+ && bluetoothManager.getState() != AppRTCBluetoothManager.State.SCO_CONNECTED
1114
+ && userSelectedAudioDevice != AudioDevice.BLUETOOTH) {
1812
1115
  newAudioDevice = getPreferredAudioDevice(true); // --- skip bluetooth
1813
1116
  }
1814
1117
 
@@ -1817,9 +1120,9 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1817
1120
 
1818
1121
  // Do the required device switch.
1819
1122
  setAudioDeviceInternal(newAudioDevice);
1820
- // Log.d(TAG, "New device status: "
1821
- // + "available=" + audioDevices + ", "
1822
- // + "selected=" + newAudioDevice);
1123
+ RTKLogger.d("InCallManager", "New device status: "
1124
+ + "available=" + audioDevices + ", "
1125
+ + "selected=" + newAudioDevice);
1823
1126
  /*
1824
1127
  if (audioManagerEvents != null) {
1825
1128
  // Notify a listening client that audio device has been changed.
@@ -1828,7 +1131,7 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
1828
1131
  */
1829
1132
  sendEvent("onAudioDeviceChanged", getAudioDeviceStatusMap());
1830
1133
  }
1831
- // Log.d(TAG, "--- updateAudioDeviceState done");
1134
+ RTKLogger.d("InCallManager", "--- updateAudioDeviceState done");
1832
1135
  });
1833
1136
  }
1834
1137