@cloudflare/realtimekit-react-native 0.0.0 → 0.0.1-staging.3

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 (83) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +83 -0
  3. package/RealtimeKitCore.podspec +19 -0
  4. package/android/build.gradle +58 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  7. package/android/gradlew +185 -0
  8. package/android/gradlew.bat +89 -0
  9. package/android/src/main/AndroidManifest.xml +23 -0
  10. package/android/src/main/java/com/reactnativecore/BackgroundTimer/RNBackgroundTimer.java +97 -0
  11. package/android/src/main/java/com/reactnativecore/Constants.java +14 -0
  12. package/android/src/main/java/com/reactnativecore/CoreModule.java +49 -0
  13. package/android/src/main/java/com/reactnativecore/CorePackage.java +34 -0
  14. package/android/src/main/java/com/reactnativecore/DyteHelperModule.java +268 -0
  15. package/android/src/main/java/com/reactnativecore/DyteHolder.java +7 -0
  16. package/android/src/main/java/com/reactnativecore/ForegroundService.java +48 -0
  17. package/android/src/main/java/com/reactnativecore/NotificationHelper.java +127 -0
  18. package/android/src/main/java/com/reactnativecore/RNPermissionsModule.java +226 -0
  19. package/android/src/main/java/com/reactnativecore/incallmanager/AppRTCBluetoothManager.java +637 -0
  20. package/android/src/main/java/com/reactnativecore/incallmanager/AppRTCProximitySensor.java +133 -0
  21. package/android/src/main/java/com/reactnativecore/incallmanager/AppRTCUtils.java +41 -0
  22. package/android/src/main/java/com/reactnativecore/incallmanager/InCallManagerModule.java +1881 -0
  23. package/android/src/main/java/com/reactnativecore/incallmanager/InCallProximityManager.java +162 -0
  24. package/android/src/main/java/com/reactnativecore/incallmanager/InCallWakeLockUtils.java +116 -0
  25. package/android/src/main/java/com/reactnativecore/incallmanager/ThreadUtils.java +39 -0
  26. package/app.plugin.js +1 -0
  27. package/ios/BroadcastEventEmitter.h +11 -0
  28. package/ios/BroadcastEventEmitter.m +72 -0
  29. package/ios/Core.h +5 -0
  30. package/ios/Core.m +28 -0
  31. package/ios/Core.xcodeproj/project.pbxproj +287 -0
  32. package/ios/DyteRNBackgroundTimer.h +22 -0
  33. package/ios/DyteRNBackgroundTimer.m +90 -0
  34. package/ios/DyteRNPermissionHandlerNotifications.h +17 -0
  35. package/ios/DyteRNPermissionHandlerNotifications.m +127 -0
  36. package/ios/DyteRNPermissions.h +50 -0
  37. package/ios/DyteRNPermissions.m +275 -0
  38. package/ios/DyteScreensharePickerView.h +8 -0
  39. package/ios/DyteScreensharePickerView.m +27 -0
  40. package/ios/PrivacyInfo.xcprivacy +60 -0
  41. package/ios/RNInCallManager.h +16 -0
  42. package/ios/RNInCallManager.m +1354 -0
  43. package/ios/permissions/DyteRNPermissionHandlerBluetoothPeripheral.h +4 -0
  44. package/ios/permissions/DyteRNPermissionHandlerBluetoothPeripheral.m +87 -0
  45. package/ios/permissions/DyteRNPermissionHandlerCamera.h +5 -0
  46. package/ios/permissions/DyteRNPermissionHandlerCamera.m +38 -0
  47. package/ios/permissions/DyteRNPermissionHandlerMicrophone.h +5 -0
  48. package/ios/permissions/DyteRNPermissionHandlerMicrophone.m +34 -0
  49. package/ios/permissions/DyteRNPermissionHandlerPhotoLibrary.h +8 -0
  50. package/ios/permissions/DyteRNPermissionHandlerPhotoLibrary.m +70 -0
  51. package/ios/screenshare/DyteDarwinNotificationCenter.swift +21 -0
  52. package/ios/screenshare/DyteScreenshareAtomic.swift +29 -0
  53. package/ios/screenshare/DyteScreenshareHandler.swift +103 -0
  54. package/ios/screenshare/DyteScreenshareUploader.swift +139 -0
  55. package/ios/screenshare/DyteSocketConnection.swift +190 -0
  56. package/lib/BackgroundHandler.d.ts +20 -0
  57. package/lib/BackgroundHandler.js +87 -0
  58. package/lib/LocalMediaError.d.ts +4 -0
  59. package/lib/LocalMediaError.js +6 -0
  60. package/lib/LocalMediaHandler.d.ts +118 -0
  61. package/lib/LocalMediaHandler.js +853 -0
  62. package/lib/LocalMediaInterfaces.d.ts +76 -0
  63. package/lib/LocalMediaInterfaces.js +1 -0
  64. package/lib/LocalMediaUtils.d.ts +93 -0
  65. package/lib/LocalMediaUtils.js +276 -0
  66. package/lib/NativeAudioManager.d.ts +16 -0
  67. package/lib/NativeAudioManager.js +93 -0
  68. package/lib/PermissionHandler.d.ts +34 -0
  69. package/lib/PermissionHandler.js +179 -0
  70. package/lib/ReactContext.d.ts +42 -0
  71. package/lib/ReactContext.js +96 -0
  72. package/lib/ReactHooks.d.ts +11 -0
  73. package/lib/ReactHooks.js +73 -0
  74. package/lib/index.d.ts +10 -0
  75. package/lib/index.js +40 -0
  76. package/lib/utils/crypto.d.ts +17 -0
  77. package/lib/utils/crypto.js +82 -0
  78. package/package.json +51 -4
  79. package/plugin/build/withDyte.d.ts +25 -0
  80. package/plugin/build/withDyte.js +100 -0
  81. package/plugin/src/withDyte.ts +150 -0
  82. package/plugin/tsconfig.json +10 -0
  83. package/plugin/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,1881 @@
1
+ package com.reactnativecore.incallmanager;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import android.content.IntentFilter;
7
+ import android.content.BroadcastReceiver;
8
+ import android.content.pm.PackageManager;
9
+ import android.media.AudioAttributes;
10
+ import android.media.AudioDeviceInfo;
11
+ import android.media.AudioFocusRequest;
12
+ import android.media.AudioManager;
13
+ import android.media.MediaPlayer;
14
+ import android.media.ToneGenerator;
15
+ import android.net.Uri;
16
+ import android.os.PowerManager;
17
+ import android.os.Build;
18
+ import android.os.Handler;
19
+ import android.os.Looper;
20
+ import android.provider.Settings;
21
+ import androidx.annotation.Nullable;
22
+ import android.util.Log;
23
+ import android.util.SparseArray;
24
+ import android.view.Display;
25
+ import android.view.KeyEvent;
26
+ import android.view.Window;
27
+ import android.view.WindowManager;
28
+ import android.media.AudioDeviceInfo;
29
+ import android.media.MediaPlayer;
30
+
31
+ import com.facebook.react.bridge.Arguments;
32
+ import com.facebook.react.bridge.LifecycleEventListener;
33
+ import com.facebook.react.bridge.NativeModule;
34
+ import com.facebook.react.bridge.Promise;
35
+ import com.facebook.react.bridge.ReactApplicationContext;
36
+ import com.facebook.react.bridge.ReactContext;
37
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
38
+ import com.facebook.react.bridge.ReactMethod;
39
+ import com.facebook.react.bridge.UiThreadUtil;
40
+ import com.facebook.react.bridge.WritableMap;
41
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
42
+
43
+ import java.lang.Runnable;
44
+ import java.io.File;
45
+ import java.util.Collections;
46
+ import java.util.Map;
47
+ import java.util.HashMap;
48
+ import java.util.HashSet;
49
+ import java.util.Random;
50
+ import java.util.Set;
51
+ import java.util.List;
52
+
53
+ import com.reactnativecore.incallmanager.AppRTCBluetoothManager;
54
+ import com.reactnativecore.incallmanager.InCallManagerModule.AudioDevice;
55
+
56
+ public class InCallManagerModule extends ReactContextBaseJavaModule implements LifecycleEventListener, AudioManager.OnAudioFocusChangeListener {
57
+ private static final String REACT_NATIVE_MODULE_NAME = "InCallManager";
58
+ private static final String TAG = REACT_NATIVE_MODULE_NAME;
59
+ private String mPackageName = "com.reactnativecore.incallmanager";
60
+
61
+ // --- Screen Manager
62
+ private PowerManager mPowerManager;
63
+ private WindowManager.LayoutParams lastLayoutParams;
64
+ private WindowManager mWindowManager;
65
+
66
+ // --- AudioRouteManager
67
+ private AudioManager audioManager;
68
+ private boolean audioManagerActivated = false;
69
+ private boolean isAudioFocused = false;
70
+ //private final Object mAudioFocusLock = new Object();
71
+ private boolean isOrigAudioSetupStored = false;
72
+ private boolean origIsSpeakerPhoneOn = false;
73
+ private boolean origIsMicrophoneMute = false;
74
+ private int origAudioMode = AudioManager.MODE_INVALID;
75
+ private boolean defaultSpeakerOn = false;
76
+ private int defaultAudioMode = AudioManager.MODE_IN_COMMUNICATION;
77
+ private int forceSpeakerOn = 0;
78
+ private boolean automatic = true;
79
+ private boolean isProximityRegistered = false;
80
+ private boolean proximityIsNear = false;
81
+ private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG;
82
+ private BroadcastReceiver wiredHeadsetReceiver;
83
+ private BroadcastReceiver noisyAudioReceiver;
84
+ private BroadcastReceiver mediaButtonReceiver;
85
+ private AudioAttributes mAudioAttributes;
86
+ private AudioFocusRequest mAudioFocusRequest;
87
+
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
+ /**
108
+ * AudioDevice is the names of possible audio devices that we currently
109
+ * support.
110
+ */
111
+ public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE }
112
+
113
+ /** AudioManager state. */
114
+ public enum AudioManagerState {
115
+ UNINITIALIZED,
116
+ PREINITIALIZED,
117
+ RUNNING,
118
+ }
119
+
120
+ private int savedAudioMode = AudioManager.MODE_INVALID;
121
+ private boolean savedIsSpeakerPhoneOn = false;
122
+ private boolean savedIsMicrophoneMute = false;
123
+ private boolean hasWiredHeadset = false;
124
+
125
+ // Default audio device; speaker phone for video calls or earpiece for audio
126
+ // only calls.
127
+ private AudioDevice defaultAudioDevice = AudioDevice.NONE;
128
+
129
+ // Contains the currently selected audio device.
130
+ // This device is changed automatically using a certain scheme where e.g.
131
+ // a wired headset "wins" over speaker phone. It is also possible for a
132
+ // user to explicitly select a device (and overrid any predefined scheme).
133
+ // See |userSelectedAudioDevice| for details.
134
+ private AudioDevice selectedAudioDevice;
135
+
136
+ // Contains the user-selected audio device which overrides the predefined
137
+ // selection scheme.
138
+ // TODO(henrika): always set to AudioDevice.NONE today. Add support for
139
+ // explicit selection based on choice by userSelectedAudioDevice.
140
+ private AudioDevice userSelectedAudioDevice;
141
+
142
+ // Contains speakerphone setting: auto, true or false
143
+ private final String useSpeakerphone = SPEAKERPHONE_AUTO;
144
+
145
+ // Handles all tasks related to Bluetooth headset devices.
146
+ private AppRTCBluetoothManager bluetoothManager = null;
147
+
148
+ private final InCallProximityManager proximityManager;
149
+
150
+ private final InCallWakeLockUtils wakeLockUtils;
151
+
152
+ // Contains a list of available audio devices. A Set collection is used to
153
+ // avoid duplicate elements.
154
+ private Set<AudioDevice> audioDevices = new HashSet<>();
155
+
156
+ interface MyPlayerInterface {
157
+ public boolean isPlaying();
158
+ public void startPlay(Map<String, Object> data);
159
+ public void stopPlay();
160
+ }
161
+
162
+ @Override
163
+ public String getName() {
164
+ return REACT_NATIVE_MODULE_NAME;
165
+ }
166
+
167
+ public InCallManagerModule(ReactApplicationContext reactContext) {
168
+ super(reactContext);
169
+ mPackageName = reactContext.getPackageName();
170
+ reactContext.addLifecycleEventListener(this);
171
+ mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
172
+ mPowerManager = (PowerManager) reactContext.getSystemService(Context.POWER_SERVICE);
173
+ 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
+ wakeLockUtils = new InCallWakeLockUtils(reactContext);
182
+ proximityManager = InCallProximityManager.create(reactContext, this);
183
+
184
+ UiThreadUtil.runOnUiThread(() -> {
185
+ bluetoothManager = AppRTCBluetoothManager.create(reactContext, this);
186
+ });
187
+
188
+ // Log.d(TAG, "InCallManager initialized");
189
+ }
190
+
191
+ private void manualTurnScreenOff() {
192
+ // Log.d(TAG, "manualTurnScreenOff()");
193
+ UiThreadUtil.runOnUiThread(new Runnable() {
194
+ public void run() {
195
+ Activity mCurrentActivity = getCurrentActivity();
196
+ if (mCurrentActivity == null) {
197
+ // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
198
+ return;
199
+ }
200
+ Window window = mCurrentActivity.getWindow();
201
+ WindowManager.LayoutParams params = window.getAttributes();
202
+ lastLayoutParams = params; // --- store last param
203
+ params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; // --- Dim as dark as possible. see BRIGHTNESS_OVERRIDE_OFF
204
+ window.setAttributes(params);
205
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
206
+ }
207
+ });
208
+ }
209
+
210
+ private void manualTurnScreenOn() {
211
+ // Log.d(TAG, "manualTurnScreenOn()");
212
+ UiThreadUtil.runOnUiThread(new Runnable() {
213
+ public void run() {
214
+ Activity mCurrentActivity = getCurrentActivity();
215
+ if (mCurrentActivity == null) {
216
+ // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
217
+ return;
218
+ }
219
+ Window window = mCurrentActivity.getWindow();
220
+ if (lastLayoutParams != null) {
221
+ window.setAttributes(lastLayoutParams);
222
+ } else {
223
+ WindowManager.LayoutParams params = window.getAttributes();
224
+ params.screenBrightness = -1; // --- Dim to preferable one
225
+ window.setAttributes(params);
226
+ }
227
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
228
+ }
229
+ });
230
+ }
231
+
232
+ private void storeOriginalAudioSetup() {
233
+ // Log.d(TAG, "storeOriginalAudioSetup()");
234
+ if (!isOrigAudioSetupStored) {
235
+ origAudioMode = audioManager.getMode();
236
+ origIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
237
+ origIsMicrophoneMute = audioManager.isMicrophoneMute();
238
+ isOrigAudioSetupStored = true;
239
+ }
240
+ }
241
+
242
+ private void restoreOriginalAudioSetup() {
243
+ // Log.d(TAG, "restoreOriginalAudioSetup()");
244
+ if (isOrigAudioSetupStored) {
245
+ setSpeakerphoneOn(origIsSpeakerPhoneOn);
246
+ setMicrophoneMute(origIsMicrophoneMute);
247
+ audioManager.setMode(origAudioMode);
248
+ if (getCurrentActivity() != null) {
249
+ getCurrentActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
250
+ }
251
+ isOrigAudioSetupStored = false;
252
+ }
253
+ }
254
+
255
+ private void startWiredHeadsetEvent() {
256
+ if (wiredHeadsetReceiver == null) {
257
+ // Log.d(TAG, "startWiredHeadsetEvent()");
258
+ IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG);
259
+ wiredHeadsetReceiver = new BroadcastReceiver() {
260
+ @Override
261
+ public void onReceive(Context context, Intent intent) {
262
+ if (ACTION_HEADSET_PLUG.equals(intent.getAction())) {
263
+ hasWiredHeadset = intent.getIntExtra("state", 0) == 1;
264
+ updateAudioRoute();
265
+ String deviceName = intent.getStringExtra("name");
266
+ if (deviceName == null) {
267
+ deviceName = "";
268
+ }
269
+ WritableMap data = Arguments.createMap();
270
+ data.putBoolean("isPlugged", (intent.getIntExtra("state", 0) == 1) ? true : false);
271
+ data.putBoolean("hasMic", (intent.getIntExtra("microphone", 0) == 1) ? true : false);
272
+ data.putString("deviceName", deviceName);
273
+ sendEvent("WiredHeadset", data);
274
+ } else {
275
+ hasWiredHeadset = false;
276
+ }
277
+ }
278
+ };
279
+ ReactContext reactContext = getReactApplicationContext();
280
+ if (reactContext != null) {
281
+ this.registerReceiver(wiredHeadsetReceiver, filter);
282
+ } else {
283
+ // Log.d(TAG, "startWiredHeadsetEvent() reactContext is null");
284
+ }
285
+ }
286
+ }
287
+
288
+ private void stopWiredHeadsetEvent() {
289
+ if (wiredHeadsetReceiver != null) {
290
+ // Log.d(TAG, "stopWiredHeadsetEvent()");
291
+ this.unregisterReceiver(this.wiredHeadsetReceiver);
292
+ wiredHeadsetReceiver = null;
293
+ }
294
+ }
295
+
296
+ private void startNoisyAudioEvent() {
297
+ if (noisyAudioReceiver == null) {
298
+ // Log.d(TAG, "startNoisyAudioEvent()");
299
+ IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
300
+ noisyAudioReceiver = new BroadcastReceiver() {
301
+ @Override
302
+ public void onReceive(Context context, Intent intent) {
303
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
304
+ updateAudioRoute();
305
+ sendEvent("NoisyAudio", null);
306
+ }
307
+ }
308
+ };
309
+ ReactContext reactContext = getReactApplicationContext();
310
+ if (reactContext != null) {
311
+ this.registerReceiver(noisyAudioReceiver, filter);
312
+ } else {
313
+ // Log.d(TAG, "startNoisyAudioEvent() reactContext is null");
314
+ }
315
+ }
316
+ }
317
+
318
+ private void stopNoisyAudioEvent() {
319
+ if (noisyAudioReceiver != null) {
320
+ // Log.d(TAG, "stopNoisyAudioEvent()");
321
+ this.unregisterReceiver(this.noisyAudioReceiver);
322
+ noisyAudioReceiver = null;
323
+ }
324
+ }
325
+
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
+ public void onProximitySensorChangedState(boolean isNear) {
394
+ if (automatic && getSelectedAudioDevice() == AudioDevice.EARPIECE) {
395
+ if (isNear) {
396
+ turnScreenOff();
397
+ } else {
398
+ turnScreenOn();
399
+ }
400
+ updateAudioRoute();
401
+ }
402
+ WritableMap data = Arguments.createMap();
403
+ data.putBoolean("isNear", isNear);
404
+ sendEvent("Proximity", data);
405
+ }
406
+
407
+ @ReactMethod
408
+ public void startProximitySensor() {
409
+ if (!proximityManager.isProximitySupported()) {
410
+ // Log.d(TAG, "Proximity Sensor is not supported.");
411
+ return;
412
+ }
413
+ if (isProximityRegistered) {
414
+ // Log.d(TAG, "Proximity Sensor is already registered.");
415
+ return;
416
+ }
417
+ // --- SENSOR_DELAY_FASTEST(0 milisecs), SENSOR_DELAY_GAME(20 milisecs), SENSOR_DELAY_UI(60 milisecs), SENSOR_DELAY_NORMAL(200 milisecs)
418
+ if (!proximityManager.start()) {
419
+ // Log.d(TAG, "proximityManager.start() failed. return false");
420
+ return;
421
+ }
422
+ // Log.d(TAG, "startProximitySensor()");
423
+ isProximityRegistered = true;
424
+ }
425
+
426
+ @ReactMethod
427
+ public void stopProximitySensor() {
428
+ if (!proximityManager.isProximitySupported()) {
429
+ // Log.d(TAG, "Proximity Sensor is not supported.");
430
+ return;
431
+ }
432
+ if (!isProximityRegistered) {
433
+ // Log.d(TAG, "Proximity Sensor is not registered.");
434
+ return;
435
+ }
436
+ // Log.d(TAG, "stopProximitySensor()");
437
+ proximityManager.stop();
438
+ isProximityRegistered = false;
439
+ }
440
+
441
+ // --- see: https://developer.android.com/reference/android/media/AudioManager
442
+ @Override
443
+ public void onAudioFocusChange(int focusChange) {
444
+ String focusChangeStr;
445
+ switch (focusChange) {
446
+ case AudioManager.AUDIOFOCUS_GAIN:
447
+ focusChangeStr = "AUDIOFOCUS_GAIN";
448
+ break;
449
+ case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
450
+ focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT";
451
+ break;
452
+ case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
453
+ focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
454
+ break;
455
+ case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
456
+ focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
457
+ break;
458
+ case AudioManager.AUDIOFOCUS_LOSS:
459
+ focusChangeStr = "AUDIOFOCUS_LOSS";
460
+ break;
461
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
462
+ focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT";
463
+ break;
464
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
465
+ focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
466
+ break;
467
+ case AudioManager.AUDIOFOCUS_NONE:
468
+ focusChangeStr = "AUDIOFOCUS_NONE";
469
+ break;
470
+ default:
471
+ focusChangeStr = "AUDIOFOCUS_UNKNOW";
472
+ break;
473
+ }
474
+
475
+ // Log.d(TAG, "onAudioFocusChange(): " + focusChange + " - " + focusChangeStr);
476
+
477
+ WritableMap data = Arguments.createMap();
478
+ data.putString("eventText", focusChangeStr);
479
+ data.putInt("eventCode", focusChange);
480
+ sendEvent("onAudioFocusChange", data);
481
+ }
482
+
483
+ private void sendEvent(final String eventName, @Nullable WritableMap params) {
484
+ try {
485
+ ReactContext reactContext = getReactApplicationContext();
486
+ if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
487
+ // Log.i(TAG, "Sending event");
488
+ reactContext
489
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
490
+ .emit(eventName, params);
491
+ } else {
492
+ Log.e(TAG, "sendEvent(): reactContext is null or not having CatalystInstance yet.");
493
+ }
494
+ } catch (RuntimeException e) {
495
+ Log.e(TAG, "sendEvent(): java.lang.RuntimeException: Trying to invoke JS before CatalystInstance has been set!");
496
+ }
497
+ }
498
+
499
+ @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;
508
+ if (!audioManagerActivated) {
509
+ audioManagerActivated = true;
510
+
511
+ // Log.d(TAG, "start audioRouteManager");
512
+ wakeLockUtils.acquirePartialWakeLock();
513
+ if (mRingtone != null && mRingtone.isPlaying()) {
514
+ // Log.d(TAG, "stop ringtone");
515
+ stopRingtone(); // --- use brandnew instance
516
+ }
517
+ storeOriginalAudioSetup();
518
+ requestAudioFocus();
519
+ startEvents();
520
+ UiThreadUtil.runOnUiThread(() -> {
521
+ bluetoothManager.start();
522
+ });
523
+ audioManager.setMode(defaultAudioMode);
524
+ setSpeakerphoneOn(defaultSpeakerOn);
525
+ setMicrophoneMute(false);
526
+ forceSpeakerOn = 0;
527
+ hasWiredHeadset = hasWiredHeadset();
528
+ defaultAudioDevice = (defaultSpeakerOn) ? AudioDevice.SPEAKER_PHONE : (hasEarpiece()) ? AudioDevice.EARPIECE : AudioDevice.SPEAKER_PHONE;
529
+ userSelectedAudioDevice = AudioDevice.NONE;
530
+ selectedAudioDevice = AudioDevice.NONE;
531
+ audioDevices.clear();
532
+ updateAudioRoute();
533
+
534
+ if (!ringbackUriType.isEmpty()) {
535
+ startRingback(ringbackUriType);
536
+ }
537
+ }
538
+ }
539
+
540
+ public void stop() {
541
+ stop("");
542
+ }
543
+
544
+ @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;
565
+ }
566
+ wakeLockUtils.releasePartialWakeLock();
567
+ }
568
+ }
569
+
570
+ private void startEvents() {
571
+ startWiredHeadsetEvent();
572
+ startNoisyAudioEvent();
573
+ startMediaButtonEvent();
574
+ startProximitySensor(); // --- proximity event always enable, but only turn screen off when audio is routing to earpiece.
575
+ setKeepScreenOn(true);
576
+ }
577
+
578
+ private void stopEvents() {
579
+ stopWiredHeadsetEvent();
580
+ stopNoisyAudioEvent();
581
+ stopMediaButtonEvent();
582
+ stopProximitySensor();
583
+ setKeepScreenOn(false);
584
+ turnScreenOn();
585
+ }
586
+
587
+ @ReactMethod
588
+ public void requestAudioFocusJS(Promise promise) {
589
+ promise.resolve(requestAudioFocus());
590
+ }
591
+
592
+ private String requestAudioFocus() {
593
+ String requestAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
594
+ ? requestAudioFocusV26()
595
+ : requestAudioFocusOld();
596
+ // Log.d(TAG, "requestAudioFocus(): res = " + requestAudioFocusResStr);
597
+ return requestAudioFocusResStr;
598
+ }
599
+
600
+ private String requestAudioFocusV26() {
601
+ if (isAudioFocused) {
602
+ return "";
603
+ }
604
+
605
+ if (mAudioAttributes == null) {
606
+ mAudioAttributes = new AudioAttributes.Builder()
607
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
608
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
609
+ .build();
610
+ }
611
+
612
+ if (mAudioFocusRequest == null) {
613
+ mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
614
+ .setAudioAttributes(mAudioAttributes)
615
+ .setAcceptsDelayedFocusGain(false)
616
+ .setWillPauseWhenDucked(false)
617
+ .setOnAudioFocusChangeListener(this)
618
+ .build();
619
+ }
620
+
621
+ int requestAudioFocusRes = audioManager.requestAudioFocus(mAudioFocusRequest);
622
+
623
+ String requestAudioFocusResStr;
624
+ switch (requestAudioFocusRes) {
625
+ case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
626
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
627
+ break;
628
+ case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
629
+ isAudioFocused = true;
630
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
631
+ break;
632
+ case AudioManager.AUDIOFOCUS_REQUEST_DELAYED:
633
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_DELAYED";
634
+ break;
635
+ default:
636
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
637
+ break;
638
+ }
639
+
640
+ return requestAudioFocusResStr;
641
+ }
642
+
643
+ private String requestAudioFocusOld() {
644
+ if (isAudioFocused) {
645
+ return "";
646
+ }
647
+
648
+ int requestAudioFocusRes = audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
649
+
650
+ String requestAudioFocusResStr;
651
+ switch (requestAudioFocusRes) {
652
+ case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
653
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
654
+ break;
655
+ case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
656
+ isAudioFocused = true;
657
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
658
+ break;
659
+ default:
660
+ requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
661
+ break;
662
+ }
663
+
664
+ return requestAudioFocusResStr;
665
+ }
666
+
667
+ @ReactMethod
668
+ public void abandonAudioFocusJS(Promise promise) {
669
+ promise.resolve(abandonAudioFocus());
670
+ }
671
+
672
+ private String abandonAudioFocus() {
673
+ String abandonAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
674
+ ? abandonAudioFocusV26()
675
+ : abandonAudioFocusOld();
676
+ // Log.d(TAG, "abandonAudioFocus(): res = " + abandonAudioFocusResStr);
677
+ return abandonAudioFocusResStr;
678
+ }
679
+
680
+ private String abandonAudioFocusV26() {
681
+ if (!isAudioFocused || mAudioFocusRequest == null) {
682
+ return "";
683
+ }
684
+
685
+ int abandonAudioFocusRes = audioManager.abandonAudioFocusRequest(mAudioFocusRequest);
686
+ String abandonAudioFocusResStr;
687
+ switch (abandonAudioFocusRes) {
688
+ case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
689
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
690
+ break;
691
+ case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
692
+ isAudioFocused = false;
693
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
694
+ break;
695
+ default:
696
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
697
+ break;
698
+ }
699
+
700
+ return abandonAudioFocusResStr;
701
+ }
702
+
703
+ private String abandonAudioFocusOld() {
704
+ if (!isAudioFocused) {
705
+ return "";
706
+ }
707
+
708
+ int abandonAudioFocusRes = audioManager.abandonAudioFocus(this);
709
+
710
+ String abandonAudioFocusResStr;
711
+ switch (abandonAudioFocusRes) {
712
+ case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
713
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
714
+ break;
715
+ case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
716
+ isAudioFocused = false;
717
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
718
+ break;
719
+ default:
720
+ abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
721
+ break;
722
+ }
723
+
724
+ return abandonAudioFocusResStr;
725
+ }
726
+
727
+ @ReactMethod
728
+ public void pokeScreen(int timeout) {
729
+ // Log.d(TAG, "pokeScreen()");
730
+ wakeLockUtils.acquirePokeFullWakeLockReleaseAfter(timeout); // --- default 3000 ms
731
+ }
732
+
733
+ private void debugScreenPowerState() {
734
+ String isDeviceIdleMode = "unknow"; // --- API 23
735
+ String isIgnoringBatteryOptimizations = "unknow"; // --- API 23
736
+ String isPowerSaveMode = "unknow"; // --- API 21
737
+ String isInteractive = "unknow"; // --- API 20 ( before since API 7 is: isScreenOn())
738
+ String screenState = "unknow"; // --- API 20
739
+
740
+ if (android.os.Build.VERSION.SDK_INT >= 23) {
741
+ isDeviceIdleMode = String.format("%s", mPowerManager.isDeviceIdleMode());
742
+ isIgnoringBatteryOptimizations = String.format("%s", mPowerManager.isIgnoringBatteryOptimizations(mPackageName));
743
+ }
744
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
745
+ isPowerSaveMode = String.format("%s", mPowerManager.isPowerSaveMode());
746
+ }
747
+ if (android.os.Build.VERSION.SDK_INT >= 20) {
748
+ isInteractive = String.format("%s", mPowerManager.isInteractive());
749
+ Display display = mWindowManager.getDefaultDisplay();
750
+ switch (display.getState()) {
751
+ case Display.STATE_OFF:
752
+ screenState = "STATE_OFF";
753
+ break;
754
+ case Display.STATE_ON:
755
+ screenState = "STATE_ON";
756
+ break;
757
+ case Display.STATE_DOZE:
758
+ screenState = "STATE_DOZE";
759
+ break;
760
+ case Display.STATE_DOZE_SUSPEND:
761
+ screenState = "STATE_DOZE_SUSPEND";
762
+ break;
763
+ default:
764
+ break;
765
+ }
766
+ } else {
767
+ isInteractive = String.format("%s", mPowerManager.isScreenOn());
768
+ }
769
+ // Log.d(TAG, String.format("debugScreenPowerState(): screenState='%s', isInteractive='%s', isPowerSaveMode='%s', isDeviceIdleMode='%s', isIgnoringBatteryOptimizations='%s'", screenState, isInteractive, isPowerSaveMode, isDeviceIdleMode, isIgnoringBatteryOptimizations));
770
+ }
771
+
772
+ @ReactMethod
773
+ public void turnScreenOn() {
774
+ if (proximityManager.isProximityWakeLockSupported()) {
775
+ // Log.d(TAG, "turnScreenOn(): use proximity lock.");
776
+ proximityManager.releaseProximityWakeLock(true);
777
+ } else {
778
+ // Log.d(TAG, "turnScreenOn(): proximity lock is not supported. try manually.");
779
+ manualTurnScreenOn();
780
+ }
781
+ }
782
+
783
+ @ReactMethod
784
+ public void turnScreenOff() {
785
+ if (proximityManager.isProximityWakeLockSupported()) {
786
+ // Log.d(TAG, "turnScreenOff(): use proximity lock.");
787
+ proximityManager.acquireProximityWakeLock();
788
+ } else {
789
+ // Log.d(TAG, "turnScreenOff(): proximity lock is not supported. try manually.");
790
+ manualTurnScreenOff();
791
+ }
792
+ }
793
+
794
+ @ReactMethod
795
+ public void setKeepScreenOn(final boolean enable) {
796
+ // Log.d(TAG, "setKeepScreenOn() " + enable);
797
+ UiThreadUtil.runOnUiThread(new Runnable() {
798
+ public void run() {
799
+ Activity mCurrentActivity = getCurrentActivity();
800
+ if (mCurrentActivity == null) {
801
+ // Log.d(TAG, "ReactContext doesn't hava any Activity attached.");
802
+ return;
803
+ }
804
+ Window window = mCurrentActivity.getWindow();
805
+ if (enable) {
806
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
807
+ } else {
808
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
809
+ }
810
+ }
811
+ });
812
+ }
813
+
814
+ @ReactMethod
815
+ public void setSpeakerphoneOn(final boolean enable) {
816
+ if (enable != audioManager.isSpeakerphoneOn()) {
817
+ // Log.d(TAG, "setSpeakerphoneOn(): " + enable);
818
+ audioManager.setSpeakerphoneOn(enable);
819
+ }
820
+ }
821
+
822
+ // --- TODO (zxcpoiu): These two api name is really confusing. should be changed.
823
+ /**
824
+ * flag: Int
825
+ * 0: use default action
826
+ * 1: force speaker on
827
+ * -1: force speaker off
828
+ */
829
+ @ReactMethod
830
+ public void setForceSpeakerphoneOn(final int flag) {
831
+ if (flag < -1 || flag > 1) {
832
+ return;
833
+ }
834
+ // Log.d(TAG, "setForceSpeakerphoneOn() flag: " + flag);
835
+ forceSpeakerOn = flag;
836
+
837
+ // --- will call updateAudioDeviceState()
838
+ // --- Note: in some devices, it may not contains specified route thus will not be effected.
839
+ if (flag == 1) {
840
+ selectAudioDevice(AudioDevice.SPEAKER_PHONE);
841
+ } else if (flag == -1) {
842
+ selectAudioDevice(AudioDevice.EARPIECE); // --- use the most common earpiece to force `speaker off`
843
+ } else {
844
+ selectAudioDevice(AudioDevice.NONE); // --- NONE will follow default route, the default route of `video` call is speaker.
845
+ }
846
+ }
847
+
848
+ // --- TODO (zxcpoiu): Implement api to let user choose audio devices
849
+
850
+ @ReactMethod
851
+ public void setMicrophoneMute(final boolean enable) {
852
+ if (enable != audioManager.isMicrophoneMute()) {
853
+ // Log.d(TAG, "setMicrophoneMute(): " + enable);
854
+ audioManager.setMicrophoneMute(enable);
855
+ }
856
+ }
857
+
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
+ @ReactMethod
1467
+ public void chooseAudioRoute(String audioRoute, Promise promise) {
1468
+ // Log.e(TAG, "RNInCallManager.chooseAudioRoute(): user choose audioDevice = " + audioRoute);
1469
+
1470
+ if (audioRoute.equals(AudioDevice.EARPIECE.name()) || audioRoute.equals("earpiece")) {
1471
+ selectAudioDevice(AudioDevice.EARPIECE);
1472
+ } else if (audioRoute.equals(AudioDevice.SPEAKER_PHONE.name()) || audioRoute.equals("speaker")) {
1473
+ selectAudioDevice(AudioDevice.SPEAKER_PHONE);
1474
+ } else if (audioRoute.equals(AudioDevice.WIRED_HEADSET.name()) || audioRoute.equals("wired")) {
1475
+ selectAudioDevice(AudioDevice.WIRED_HEADSET);
1476
+ } else if (audioRoute.equals(AudioDevice.BLUETOOTH.name()) || audioRoute.equals("bluetooth")) {
1477
+ selectAudioDevice(AudioDevice.BLUETOOTH);
1478
+ }
1479
+ promise.resolve(getAudioDeviceStatusMap());
1480
+ }
1481
+
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
+ private void pause() {
1491
+ if (audioManagerActivated) {
1492
+ // Log.d(TAG, "pause audioRouteManager");
1493
+ stopEvents();
1494
+ }
1495
+ }
1496
+
1497
+ private void resume() {
1498
+ if (audioManagerActivated) {
1499
+ // Log.d(TAG, "resume audioRouteManager");
1500
+ startEvents();
1501
+ }
1502
+ }
1503
+
1504
+ @Override
1505
+ public void onHostResume() {
1506
+ // Log.d(TAG, "onResume()");
1507
+ //resume();
1508
+ }
1509
+
1510
+ @Override
1511
+ public void onHostPause() {
1512
+ // Log.d(TAG, "onPause()");
1513
+ //pause();
1514
+ }
1515
+
1516
+ @Override
1517
+ public void onHostDestroy() {
1518
+ // Log.d(TAG, "onDestroy()");
1519
+ stopRingtone();
1520
+ stopRingback();
1521
+ stopBusytone();
1522
+ stop();
1523
+ }
1524
+
1525
+ private void updateAudioRoute() {
1526
+ if (!automatic) {
1527
+ return;
1528
+ }
1529
+ updateAudioDeviceState();
1530
+ }
1531
+
1532
+ // ===== NOTE: below functions is based on appRTC DEMO M64 ===== //
1533
+ /** Changes selection of the currently active audio device. */
1534
+ private void setAudioDeviceInternal(AudioDevice device) {
1535
+ // Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")");
1536
+ if (!audioDevices.contains(device)) {
1537
+ // Log.e(TAG, "specified audio device does not exist");
1538
+ return;
1539
+ }
1540
+ switch (device) {
1541
+ case SPEAKER_PHONE:
1542
+ setSpeakerphoneOn(true);
1543
+ audioManager.setWiredHeadsetOn(false);
1544
+ stopProximitySensor();
1545
+ break;
1546
+ case EARPIECE:
1547
+ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
1548
+ setSpeakerphoneOn(false);
1549
+ startProximitySensor();
1550
+ audioManager.setWiredHeadsetOn(false);
1551
+ break;
1552
+ case WIRED_HEADSET:
1553
+ audioManager.setWiredHeadsetOn(true);
1554
+ setSpeakerphoneOn(false);
1555
+ stopProximitySensor();
1556
+ break;
1557
+ case BLUETOOTH:
1558
+ setSpeakerphoneOn(false);
1559
+ audioManager.setWiredHeadsetOn(false);
1560
+ stopProximitySensor();
1561
+ break;
1562
+ default:
1563
+ // Log.e(TAG, "Invalid audio device selection");
1564
+ break;
1565
+ }
1566
+ selectedAudioDevice = device;
1567
+ }
1568
+
1569
+
1570
+
1571
+ /**
1572
+ * Changes default audio device.
1573
+ * TODO(henrika): add usage of this method in the AppRTCMobile client.
1574
+ */
1575
+ public void setDefaultAudioDevice(AudioDevice defaultDevice) {
1576
+ switch (defaultDevice) {
1577
+ case SPEAKER_PHONE:
1578
+ defaultAudioDevice = defaultDevice;
1579
+ break;
1580
+ case EARPIECE:
1581
+ if (hasEarpiece()) {
1582
+ defaultAudioDevice = defaultDevice;
1583
+ } else {
1584
+ defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
1585
+ }
1586
+ break;
1587
+ default:
1588
+ // Log.e(TAG, "Invalid default audio device selection");
1589
+ break;
1590
+ }
1591
+ // Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
1592
+ updateAudioDeviceState();
1593
+ }
1594
+
1595
+ /** Changes selection of the currently active audio device. */
1596
+ public void selectAudioDevice(AudioDevice device) {
1597
+ // Log.e(TAG,"Selecting "+device.name());
1598
+ userSelectedAudioDevice = device;
1599
+ updateAudioDeviceState();
1600
+ if (device != AudioDevice.NONE && !audioDevices.contains(device)) {
1601
+ // Log.e(TAG, "selectAudioDevice() Can not select " + device + " from available " + audioDevices);
1602
+ return;
1603
+ }
1604
+ }
1605
+
1606
+ /** Returns current set of available/selectable audio devices. */
1607
+ public Set<AudioDevice> getAudioDevices() {
1608
+ return Collections.unmodifiableSet(new HashSet<>(audioDevices));
1609
+ }
1610
+
1611
+ /** Returns the currently selected audio device. */
1612
+ public AudioDevice getSelectedAudioDevice() {
1613
+ return selectedAudioDevice;
1614
+ }
1615
+
1616
+ /** Helper method for receiver registration. */
1617
+ private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
1618
+ ReactContext reactContext = getReactApplicationContext();
1619
+ if (reactContext != null) {
1620
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1621
+ reactContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
1622
+ } else {
1623
+ reactContext.registerReceiver(receiver, filter);
1624
+ }
1625
+ }
1626
+ }
1627
+
1628
+ /** Helper method for unregistration of an existing receiver. */
1629
+ private void unregisterReceiver(final BroadcastReceiver receiver) {
1630
+ final ReactContext reactContext = this.getReactApplicationContext();
1631
+ if (reactContext != null) {
1632
+ try {
1633
+ reactContext.unregisterReceiver(receiver);
1634
+ } catch (final Exception e) {
1635
+ // Log.d(TAG, "unregisterReceiver() failed");
1636
+ }
1637
+ } 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;
1659
+ }
1660
+ audioManager.setMicrophoneMute(on);
1661
+ }
1662
+ */
1663
+
1664
+ /** Gets the current earpiece state. */
1665
+ private boolean hasEarpiece() {
1666
+ return getReactApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
1667
+ }
1668
+
1669
+ /**
1670
+ * Checks whether a wired headset is connected or not.
1671
+ * This is not a valid indication that audio playback is actually over
1672
+ * the wired headset as audio routing depends on other conditions. We
1673
+ * only use it as an early indicator (during initialization) of an attached
1674
+ * wired headset.
1675
+ */
1676
+ @Deprecated
1677
+ private boolean hasWiredHeadset() {
1678
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1679
+ return audioManager.isWiredHeadsetOn();
1680
+ } else {
1681
+ final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
1682
+ for (AudioDeviceInfo device : devices) {
1683
+ final int type = device.getType();
1684
+ if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
1685
+ // Log.d(TAG, "hasWiredHeadset: found wired headset");
1686
+ return true;
1687
+ } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
1688
+ // Log.d(TAG, "hasWiredHeadset: found USB audio device");
1689
+ return true;
1690
+ } else if (type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
1691
+ // Log.d(TAG, "hasWiredHeadset: found wired headphones");
1692
+ return true;
1693
+ }
1694
+ }
1695
+ return false;
1696
+ }
1697
+ }
1698
+
1699
+ @ReactMethod
1700
+ public void getIsWiredHeadsetPluggedIn(Promise promise) {
1701
+ promise.resolve(this.hasWiredHeadset());
1702
+ }
1703
+
1704
+ @ReactMethod
1705
+ public void isBluetoothHeadsetConnected(Promise promise) {
1706
+ UiThreadUtil.runOnUiThread(() -> {
1707
+ try {
1708
+ if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
1709
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
1710
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
1711
+ promise.resolve(true);
1712
+ // Log.i("Bluetooth State:", "isBluetoothHeadsetConnected: true");
1713
+ } else {
1714
+ promise.resolve(false);
1715
+ // Log.i("Bluetooth State:", "isBluetoothHeadsetConnected: false");
1716
+ }
1717
+ } catch (Exception e) {
1718
+ promise.resolve(false);
1719
+ Log.e("Bluetooth State:", "isBluetoothHeadsetConnected: false due to: ", e);
1720
+ }
1721
+ });
1722
+ }
1723
+
1724
+
1725
+ /**
1726
+ * Updates list of possible audio devices and make new device selection.
1727
+ */
1728
+ public void updateAudioDeviceState() {
1729
+ UiThreadUtil.runOnUiThread(() -> {
1730
+ // Log.e("Bluetooth State: ",bluetoothManager.getState().toString());
1731
+ if(bluetoothManager.getState().toString().equals("UNINITIALIZED"))
1732
+ bluetoothManager.start();
1733
+ 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);
1741
+
1742
+ // Check if any Bluetooth headset is connected. The internal BT state will
1743
+ // change accordingly.
1744
+ if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
1745
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
1746
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
1747
+ bluetoothManager.updateDevice();
1748
+ }
1749
+
1750
+ // Update the set of available audio devices.
1751
+ Set<AudioDevice> newAudioDevices = new HashSet<>();
1752
+
1753
+ // always assume device has speaker phone
1754
+ newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
1755
+
1756
+ if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
1757
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
1758
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
1759
+ newAudioDevices.add(AudioDevice.BLUETOOTH);
1760
+ }
1761
+
1762
+ if (hasWiredHeadset) {
1763
+ newAudioDevices.add(AudioDevice.WIRED_HEADSET);
1764
+ }
1765
+
1766
+ if (hasEarpiece()) {
1767
+ newAudioDevices.add(AudioDevice.EARPIECE);
1768
+ }
1769
+
1770
+ // --- check whether user selected audio device is available
1771
+ if (userSelectedAudioDevice != null
1772
+ && userSelectedAudioDevice != AudioDevice.NONE
1773
+ && !newAudioDevices.contains(userSelectedAudioDevice)) {
1774
+ userSelectedAudioDevice = AudioDevice.NONE;
1775
+ }
1776
+
1777
+ // Store state which is set to true if the device list has changed.
1778
+ boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
1779
+ // Update the existing audio device set.
1780
+ audioDevices = newAudioDevices;
1781
+
1782
+ AudioDevice newAudioDevice = getPreferredAudioDevice();
1783
+
1784
+ // --- stop bluetooth if needed
1785
+ if (selectedAudioDevice == AudioDevice.BLUETOOTH
1786
+ && newAudioDevice != AudioDevice.BLUETOOTH
1787
+ && (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
1788
+ || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
1789
+ ) {
1790
+ bluetoothManager.stopScoAudio();
1791
+ bluetoothManager.updateDevice();
1792
+ }
1793
+
1794
+ // --- start bluetooth if needed
1795
+ if (selectedAudioDevice != AudioDevice.BLUETOOTH
1796
+ && newAudioDevice == AudioDevice.BLUETOOTH
1797
+ && bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
1798
+ // Attempt to start Bluetooth SCO audio (takes a few second to start).
1799
+ if (!bluetoothManager.startScoAudio()) {
1800
+ // Remove BLUETOOTH from list of available devices since SCO failed.
1801
+ audioDevices.remove(AudioDevice.BLUETOOTH);
1802
+ audioDeviceSetUpdated = true;
1803
+ if (userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
1804
+ userSelectedAudioDevice = AudioDevice.NONE;
1805
+ }
1806
+ newAudioDevice = getPreferredAudioDevice();
1807
+ }
1808
+ }
1809
+
1810
+ if (newAudioDevice == AudioDevice.BLUETOOTH
1811
+ && bluetoothManager.getState() != AppRTCBluetoothManager.State.SCO_CONNECTED) {
1812
+ newAudioDevice = getPreferredAudioDevice(true); // --- skip bluetooth
1813
+ }
1814
+
1815
+ // Switch to new device but only if there has been any changes.
1816
+ if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
1817
+
1818
+ // Do the required device switch.
1819
+ setAudioDeviceInternal(newAudioDevice);
1820
+ // Log.d(TAG, "New device status: "
1821
+ // + "available=" + audioDevices + ", "
1822
+ // + "selected=" + newAudioDevice);
1823
+ /*
1824
+ if (audioManagerEvents != null) {
1825
+ // Notify a listening client that audio device has been changed.
1826
+ audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
1827
+ }
1828
+ */
1829
+ sendEvent("onAudioDeviceChanged", getAudioDeviceStatusMap());
1830
+ }
1831
+ // Log.d(TAG, "--- updateAudioDeviceState done");
1832
+ });
1833
+ }
1834
+
1835
+ private WritableMap getAudioDeviceStatusMap() {
1836
+ WritableMap data = Arguments.createMap();
1837
+ String audioDevicesJson = "[";
1838
+ for (AudioDevice s: audioDevices) {
1839
+ audioDevicesJson += s.name() + ",";
1840
+ }
1841
+
1842
+ // --- strip the last `,`
1843
+ if (audioDevicesJson.length() > 1) {
1844
+ audioDevicesJson = audioDevicesJson.substring(0, audioDevicesJson.length() - 1);
1845
+ }
1846
+ audioDevicesJson += "]";
1847
+
1848
+ data.putString("availableAudioDeviceList", audioDevicesJson);
1849
+ data.putString("selectedAudioDevice", (selectedAudioDevice == null) ? "" : selectedAudioDevice.name());
1850
+
1851
+ return data;
1852
+ }
1853
+
1854
+ private AudioDevice getPreferredAudioDevice() {
1855
+ return getPreferredAudioDevice(false);
1856
+ }
1857
+
1858
+ private AudioDevice getPreferredAudioDevice(boolean skipBluetooth) {
1859
+ final AudioDevice newAudioDevice;
1860
+
1861
+ if (userSelectedAudioDevice != null && userSelectedAudioDevice != AudioDevice.NONE) {
1862
+ newAudioDevice = userSelectedAudioDevice;
1863
+ } else if (!skipBluetooth && audioDevices.contains(AudioDevice.BLUETOOTH)) {
1864
+ // If a Bluetooth is connected, then it should be used as output audio
1865
+ // device. Note that it is not sufficient that a headset is available;
1866
+ // an active SCO channel must also be up and running.
1867
+ newAudioDevice = AudioDevice.BLUETOOTH;
1868
+ } else if (audioDevices.contains(AudioDevice.WIRED_HEADSET)) {
1869
+ // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
1870
+ // audio device.
1871
+ newAudioDevice = AudioDevice.WIRED_HEADSET;
1872
+ } else if (audioDevices.contains(defaultAudioDevice)) {
1873
+ newAudioDevice = defaultAudioDevice;
1874
+ } else {
1875
+ newAudioDevice = AudioDevice.SPEAKER_PHONE;
1876
+ }
1877
+
1878
+ return newAudioDevice;
1879
+ }
1880
+ }
1881
+