/* * Copyright (c) 2017 Henry Lin @zxcpoiu * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package com.zxcpoiu.incallmanager; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.content.pm.PackageManager; import android.Manifest.permission; //import android.media.AudioAttributes; // --- for API 21+ import android.media.AudioManager; import android.media.AudioDeviceInfo; import android.media.MediaPlayer; import android.media.ToneGenerator; import android.net.Uri; import android.os.PowerManager; import android.os.Build; import android.os.Handler; import android.provider.Settings; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.lang.Runnable; import java.io.File; import java.util.Collections; import java.util.Map; import java.util.HashMap; import java.util.HashSet; import java.util.Random; import java.util.Set; import com.zxcpoiu.incallmanager.AppRTC.AppRTCBluetoothManager; public class InCallManagerModule extends ReactContextBaseJavaModule implements LifecycleEventListener { private static final String REACT_NATIVE_MODULE_NAME = "InCallManager"; private static final String TAG = REACT_NATIVE_MODULE_NAME; private static SparseArray<Promise> mRequestPermissionCodePromises; private static SparseArray<String> mRequestPermissionCodeTargetPermission; private String mPackageName = "com.zxcpoiu.incallmanager"; // --- Screen Manager private PowerManager mPowerManager; private WindowManager.LayoutParams lastLayoutParams; private WindowManager mWindowManager; // --- AudioRouteManager private AudioManager audioManager; private boolean audioManagerActivated = false; private boolean isAudioFocused = false; private boolean isOrigAudioSetupStored = false; private boolean origIsSpeakerPhoneOn = false; private boolean origIsMicrophoneMute = false; private int origAudioMode = AudioManager.MODE_INVALID; private boolean defaultSpeakerOn = false; private int defaultAudioMode = AudioManager.MODE_IN_COMMUNICATION; private int forceSpeakerOn = 0; private boolean automatic = true; private boolean isProximityRegistered = false; private boolean proximityIsNear = false; private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; private BroadcastReceiver wiredHeadsetReceiver; private BroadcastReceiver noisyAudioReceiver; private BroadcastReceiver mediaButtonReceiver; private OnFocusChangeListener mOnFocusChangeListener; // --- same as: RingtoneManager.getActualDefaultRingtoneUri(reactContext, RingtoneManager.TYPE_RINGTONE); private Uri defaultRingtoneUri = Settings.System.DEFAULT_RINGTONE_URI; private Uri defaultRingbackUri = Settings.System.DEFAULT_RINGTONE_URI; private Uri defaultBusytoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; //private Uri defaultAlarmAlertUri = Settings.System.DEFAULT_ALARM_ALERT_URI; // --- too annoying private Uri bundleRingtoneUri; private Uri bundleRingbackUri; private Uri bundleBusytoneUri; private Map<String, Uri> audioUriMap; private MyPlayerInterface mRingtone; private MyPlayerInterface mRingback; private MyPlayerInterface mBusytone; private Handler mRingtoneCountDownHandler; private String media = "audio"; private static String recordPermission = "unknow"; private static String cameraPermission = "unknow"; private static final String SPEAKERPHONE_AUTO = "auto"; private static final String SPEAKERPHONE_TRUE = "true"; private static final String SPEAKERPHONE_FALSE = "false"; /** * AudioDevice is the names of possible audio devices that we currently * support. */ public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE } /** AudioManager state. */ public enum AudioManagerState { UNINITIALIZED, PREINITIALIZED, RUNNING, } private int savedAudioMode = AudioManager.MODE_INVALID; private boolean savedIsSpeakerPhoneOn = false; private boolean savedIsMicrophoneMute = false; private boolean hasWiredHeadset = false; // Default audio device; speaker phone for video calls or earpiece for audio // only calls. private AudioDevice defaultAudioDevice = AudioDevice.NONE; // Contains the currently selected audio device. // This device is changed automatically using a certain scheme where e.g. // a wired headset "wins" over speaker phone. It is also possible for a // user to explicitly select a device (and overrid any predefined scheme). // See |userSelectedAudioDevice| for details. private AudioDevice selectedAudioDevice; // Contains the user-selected audio device which overrides the predefined // selection scheme. // TODO(henrika): always set to AudioDevice.NONE today. Add support for // explicit selection based on choice by userSelectedAudioDevice. private AudioDevice userSelectedAudioDevice; // Contains speakerphone setting: auto, true or false private final String useSpeakerphone = SPEAKERPHONE_AUTO; // Handles all tasks related to Bluetooth headset devices. private final AppRTCBluetoothManager bluetoothManager; private final InCallProximityManager proximityManager; private final InCallWakeLockUtils wakeLockUtils; // Contains a list of available audio devices. A Set collection is used to // avoid duplicate elements. private Set<AudioDevice> audioDevices = new HashSet<>(); // Callback method for changes in audio focus. private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; interface MyPlayerInterface { public boolean isPlaying(); public void startPlay(Map<String, Object> data); public void stopPlay(); } @Override public String getName() { return REACT_NATIVE_MODULE_NAME; } public InCallManagerModule(ReactApplicationContext reactContext) { super(reactContext); mPackageName = reactContext.getPackageName(); reactContext.addLifecycleEventListener(this); mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE); mPowerManager = (PowerManager) reactContext.getSystemService(Context.POWER_SERVICE); audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); audioUriMap = new HashMap<String, Uri>(); audioUriMap.put("defaultRingtoneUri", defaultRingtoneUri); audioUriMap.put("defaultRingbackUri", defaultRingbackUri); audioUriMap.put("defaultBusytoneUri", defaultBusytoneUri); audioUriMap.put("bundleRingtoneUri", bundleRingtoneUri); audioUriMap.put("bundleRingbackUri", bundleRingbackUri); audioUriMap.put("bundleBusytoneUri", bundleBusytoneUri); mRequestPermissionCodePromises = new SparseArray<Promise>(); mRequestPermissionCodeTargetPermission = new SparseArray<String>(); mOnFocusChangeListener = new OnFocusChangeListener(); bluetoothManager = AppRTCBluetoothManager.create(reactContext, this); proximityManager = InCallProximityManager.create(reactContext, this); wakeLockUtils = new InCallWakeLockUtils(reactContext); Log.d(TAG, "InCallManager initialized"); } private void manualTurnScreenOff() { Log.d(TAG, "manualTurnScreenOff()"); UiThreadUtil.runOnUiThread(new Runnable() { public void run() { Activity mCurrentActivity = getCurrentActivity(); if (mCurrentActivity == null) { Log.d(TAG, "ReactContext doesn't hava any Activity attached."); return; } Window window = mCurrentActivity.getWindow(); WindowManager.LayoutParams params = window.getAttributes(); lastLayoutParams = params; // --- store last param params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; // --- Dim as dark as possible. see BRIGHTNESS_OVERRIDE_OFF window.setAttributes(params); window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } }); } private void manualTurnScreenOn() { Log.d(TAG, "manualTurnScreenOn()"); UiThreadUtil.runOnUiThread(new Runnable() { public void run() { Activity mCurrentActivity = getCurrentActivity(); if (mCurrentActivity == null) { Log.d(TAG, "ReactContext doesn't hava any Activity attached."); return; } Window window = mCurrentActivity.getWindow(); if (lastLayoutParams != null) { window.setAttributes(lastLayoutParams); } else { WindowManager.LayoutParams params = window.getAttributes(); params.screenBrightness = -1; // --- Dim to preferable one window.setAttributes(params); } window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } }); } private void storeOriginalAudioSetup() { Log.d(TAG, "storeOriginalAudioSetup()"); if (!isOrigAudioSetupStored) { origAudioMode = audioManager.getMode(); origIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); origIsMicrophoneMute = audioManager.isMicrophoneMute(); isOrigAudioSetupStored = true; } } private void restoreOriginalAudioSetup() { Log.d(TAG, "restoreOriginalAudioSetup()"); if (isOrigAudioSetupStored) { setSpeakerphoneOn(origIsSpeakerPhoneOn); setMicrophoneMute(origIsMicrophoneMute); audioManager.setMode(origAudioMode); if (getCurrentActivity() != null) { getCurrentActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); } isOrigAudioSetupStored = false; } } private void startWiredHeadsetEvent() { if (wiredHeadsetReceiver == null) { Log.d(TAG, "startWiredHeadsetEvent()"); IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG); wiredHeadsetReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ACTION_HEADSET_PLUG.equals(intent.getAction())) { hasWiredHeadset = intent.getIntExtra("state", 0) == 1; updateAudioRoute(); String deviceName = intent.getStringExtra("name"); if (deviceName == null) { deviceName = ""; } WritableMap data = Arguments.createMap(); data.putBoolean("isPlugged", (intent.getIntExtra("state", 0) == 1) ? true : false); data.putBoolean("hasMic", (intent.getIntExtra("microphone", 0) == 1) ? true : false); data.putString("deviceName", deviceName); sendEvent("WiredHeadset", data); } else { hasWiredHeadset = false; } } }; ReactContext reactContext = getReactApplicationContext(); if (reactContext != null) { reactContext.registerReceiver(wiredHeadsetReceiver, filter); } else { Log.d(TAG, "startWiredHeadsetEvent() reactContext is null"); } } } private void stopWiredHeadsetEvent() { if (wiredHeadsetReceiver != null) { Log.d(TAG, "stopWiredHeadsetEvent()"); this.unregisterReceiver(this.wiredHeadsetReceiver); wiredHeadsetReceiver = null; } } private void startNoisyAudioEvent() { if (noisyAudioReceiver == null) { Log.d(TAG, "startNoisyAudioEvent()"); IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); noisyAudioReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { updateAudioRoute(); sendEvent("NoisyAudio", null); } } }; ReactContext reactContext = getReactApplicationContext(); if (reactContext != null) { reactContext.registerReceiver(noisyAudioReceiver, filter); } else { Log.d(TAG, "startNoisyAudioEvent() reactContext is null"); } } } private void stopNoisyAudioEvent() { if (noisyAudioReceiver != null) { Log.d(TAG, "stopNoisyAudioEvent()"); this.unregisterReceiver(this.noisyAudioReceiver); noisyAudioReceiver = null; } } private void startMediaButtonEvent() { if (mediaButtonReceiver == null) { Log.d(TAG, "startMediaButtonEvent()"); IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); mediaButtonReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); int keyCode = event.getKeyCode(); String keyText = ""; switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY: keyText = "KEYCODE_MEDIA_PLAY"; break; case KeyEvent.KEYCODE_MEDIA_PAUSE: keyText = "KEYCODE_MEDIA_PAUSE"; break; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: keyText = "KEYCODE_MEDIA_PLAY_PAUSE"; break; case KeyEvent.KEYCODE_MEDIA_NEXT: keyText = "KEYCODE_MEDIA_NEXT"; break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: keyText = "KEYCODE_MEDIA_PREVIOUS"; break; case KeyEvent.KEYCODE_MEDIA_CLOSE: keyText = "KEYCODE_MEDIA_CLOSE"; break; case KeyEvent.KEYCODE_MEDIA_EJECT: keyText = "KEYCODE_MEDIA_EJECT"; break; case KeyEvent.KEYCODE_MEDIA_RECORD: keyText = "KEYCODE_MEDIA_RECORD"; break; case KeyEvent.KEYCODE_MEDIA_STOP: keyText = "KEYCODE_MEDIA_STOP"; break; default: keyText = "KEYCODE_UNKNOW"; break; } WritableMap data = Arguments.createMap(); data.putString("eventText", keyText); data.putInt("eventCode", keyCode); sendEvent("MediaButton", data); } } }; ReactContext reactContext = getReactApplicationContext(); if (reactContext != null) { reactContext.registerReceiver(mediaButtonReceiver, filter); } else { Log.d(TAG, "startMediaButtonEvent() reactContext is null"); } } } private void stopMediaButtonEvent() { if (mediaButtonReceiver != null) { Log.d(TAG, "stopMediaButtonEvent()"); this.unregisterReceiver(this.mediaButtonReceiver); mediaButtonReceiver = null; } } public void onProximitySensorChangedState(boolean isNear) { if (automatic && getSelectedAudioDevice() == AudioDevice.EARPIECE) { if (isNear) { turnScreenOff(); } else { turnScreenOn(); } updateAudioRoute(); } WritableMap data = Arguments.createMap(); data.putBoolean("isNear", isNear); sendEvent("Proximity", data); } private void startProximitySensor() { if (!proximityManager.isProximitySupported()) { Log.d(TAG, "Proximity Sensor is not supported."); return; } if (isProximityRegistered) { Log.d(TAG, "Proximity Sensor is already registered."); return; } // --- SENSOR_DELAY_FASTEST(0 milisecs), SENSOR_DELAY_GAME(20 milisecs), SENSOR_DELAY_UI(60 milisecs), SENSOR_DELAY_NORMAL(200 milisecs) if (!proximityManager.start()) { Log.d(TAG, "proximityManager.start() failed. return false"); return; } Log.d(TAG, "startProximitySensor()"); isProximityRegistered = true; } private void stopProximitySensor() { if (!proximityManager.isProximitySupported()) { Log.d(TAG, "Proximity Sensor is not supported."); return; } if (!isProximityRegistered) { Log.d(TAG, "Proximity Sensor is not registered."); return; } Log.d(TAG, "stopProximitySensor()"); proximityManager.stop(); isProximityRegistered = false; } private class OnFocusChangeListener implements AudioManager.OnAudioFocusChangeListener { @Override public void onAudioFocusChange(final int focusChange) { String focusChangeStr; switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: focusChangeStr = "AUDIOFOCUS_GAIN"; break; case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT"; break; case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; break; case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; break; case AudioManager.AUDIOFOCUS_LOSS: focusChangeStr = "AUDIOFOCUS_LOSS"; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT"; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; break; default: focusChangeStr = "AUDIOFOCUS_UNKNOW"; break; } Log.d(TAG, "onAudioFocusChange: " + focusChange + " - " + focusChangeStr); WritableMap data = Arguments.createMap(); data.putString("eventText", focusChangeStr); data.putInt("eventCode", focusChange); sendEvent("onAudioFocusChange", data); } } /* // --- TODO: AudioDeviceCallBack android sdk 23+ if (android.os.Build.VERSION.SDK_INT >= 23) { private class MyAudioDeviceCallback extends AudioDeviceCallback { public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { mAddCallbackCalled = true; } public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { mRemoveCallbackCalled = true; } } // --- Specifies the Handler object for the thread on which to execute the callback. If null, the Handler associated with the main Looper will be used. public void test_deviceCallback() { AudioDeviceCallback callback = new EmptyDeviceCallback(); mAudioManager.registerAudioDeviceCallback(callback, null); } // --- get all audio devices by flags //public AudioDeviceInfo[] getDevices (int flags) //Returns an array of AudioDeviceInfo objects corresponding to the audio devices currently connected to the system and meeting the criteria specified in the flags parameter. //flags int: A set of bitflags specifying the criteria to test. } // --- TODO: adjust valume if needed. if (android.os.Build.VERSION.SDK_INT >= 21) { isVolumeFixed () // The following APIs have no effect when volume is fixed: adjustVolume(int, int) adjustSuggestedStreamVolume(int, int, int) adjustStreamVolume(int, int, int) setStreamVolume(int, int, int) setRingerMode(int) setStreamSolo(int, boolean) setStreamMute(int, boolean) } // -- TODO: bluetooth support */ private void sendEvent(final String eventName, @Nullable WritableMap params) { try { ReactContext reactContext = getReactApplicationContext(); if (reactContext != null && reactContext.hasActiveCatalystInstance()) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } else { Log.e(TAG, "sendEvent(): reactContext is null or not having CatalystInstance yet."); } } catch (RuntimeException e) { Log.e(TAG, "sendEvent(): java.lang.RuntimeException: Trying to invoke JS before CatalystInstance has been set!"); } } @ReactMethod public void start(final String _media, final boolean auto, final String ringbackUriType) { media = _media; if (media.equals("video")) { defaultSpeakerOn = true; } else { defaultSpeakerOn = false; } automatic = auto; if (!audioManagerActivated) { audioManagerActivated = true; Log.d(TAG, "start audioRouteManager"); wakeLockUtils.acquirePartialWakeLock(); if (mRingtone != null && mRingtone.isPlaying()) { Log.d(TAG, "stop ringtone"); stopRingtone(); // --- use brandnew instance } storeOriginalAudioSetup(); requestAudioFocus(); startEvents(); bluetoothManager.start(); // TODO: even if not acquired focus, we can still play sounds. but need figure out which is better. //getCurrentActivity().setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); audioManager.setMode(defaultAudioMode); setSpeakerphoneOn(defaultSpeakerOn); setMicrophoneMute(false); forceSpeakerOn = 0; hasWiredHeadset = hasWiredHeadset(); defaultAudioDevice = (defaultSpeakerOn) ? AudioDevice.SPEAKER_PHONE : (hasEarpiece()) ? AudioDevice.EARPIECE : AudioDevice.SPEAKER_PHONE; userSelectedAudioDevice = AudioDevice.NONE; selectedAudioDevice = AudioDevice.NONE; audioDevices.clear(); updateAudioRoute(); if (!ringbackUriType.isEmpty()) { startRingback(ringbackUriType); } } } public void stop() { stop(""); } @ReactMethod public void stop(final String busytoneUriType) { if (audioManagerActivated) { stopRingback(); if (!busytoneUriType.isEmpty() && startBusytone(busytoneUriType)) { // play busytone first, and call this func again when finish Log.d(TAG, "play busytone before stop InCallManager"); return; } else { Log.d(TAG, "stop() InCallManager"); stopBusytone(); stopEvents(); setSpeakerphoneOn(false); setMicrophoneMute(false); forceSpeakerOn = 0; bluetoothManager.stop(); restoreOriginalAudioSetup(); releaseAudioFocus(); audioManagerActivated = false; } wakeLockUtils.releasePartialWakeLock(); } } private void startEvents() { startWiredHeadsetEvent(); startNoisyAudioEvent(); startMediaButtonEvent(); startProximitySensor(); // --- proximity event always enable, but only turn screen off when audio is routing to earpiece. setKeepScreenOn(true); } private void stopEvents() { stopWiredHeadsetEvent(); stopNoisyAudioEvent(); stopMediaButtonEvent(); stopProximitySensor(); setKeepScreenOn(false); turnScreenOn(); } private void requestAudioFocus() { if (!isAudioFocused) { int result = audioManager.requestAudioFocus(mOnFocusChangeListener, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(TAG, "AudioFocus granted"); isAudioFocused = true; } else if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { Log.d(TAG, "AudioFocus failed"); isAudioFocused = false; } } } private void releaseAudioFocus() { if (isAudioFocused) { audioManager.abandonAudioFocus(null); isAudioFocused = false; } } @ReactMethod public void pokeScreen(int timeout) { Log.d(TAG, "pokeScreen()"); wakeLockUtils.acquirePokeFullWakeLockReleaseAfter(timeout); // --- default 3000 ms } private void debugScreenPowerState() { String isDeviceIdleMode = "unknow"; // --- API 23 String isIgnoringBatteryOptimizations = "unknow"; // --- API 23 String isPowerSaveMode = "unknow"; // --- API 21 String isInteractive = "unknow"; // --- API 20 ( before since API 7 is: isScreenOn()) String screenState = "unknow"; // --- API 20 if (android.os.Build.VERSION.SDK_INT >= 23) { isDeviceIdleMode = String.format("%s", mPowerManager.isDeviceIdleMode()); isIgnoringBatteryOptimizations = String.format("%s", mPowerManager.isIgnoringBatteryOptimizations(mPackageName)); } if (android.os.Build.VERSION.SDK_INT >= 21) { isPowerSaveMode = String.format("%s", mPowerManager.isPowerSaveMode()); } if (android.os.Build.VERSION.SDK_INT >= 20) { isInteractive = String.format("%s", mPowerManager.isInteractive()); Display display = mWindowManager.getDefaultDisplay(); switch (display.getState()) { case Display.STATE_OFF: screenState = "STATE_OFF"; break; case Display.STATE_ON: screenState = "STATE_ON"; break; case Display.STATE_DOZE: screenState = "STATE_DOZE"; break; case Display.STATE_DOZE_SUSPEND: screenState = "STATE_DOZE_SUSPEND"; break; default: break; } } else { isInteractive = String.format("%s", mPowerManager.isScreenOn()); } Log.d(TAG, String.format("debugScreenPowerState(): screenState='%s', isInteractive='%s', isPowerSaveMode='%s', isDeviceIdleMode='%s', isIgnoringBatteryOptimizations='%s'", screenState, isInteractive, isPowerSaveMode, isDeviceIdleMode, isIgnoringBatteryOptimizations)); } @ReactMethod public void turnScreenOn() { if (proximityManager.isProximityWakeLockSupported()) { Log.d(TAG, "turnScreenOn(): use proximity lock."); proximityManager.releaseProximityWakeLock(true); } else { Log.d(TAG, "turnScreenOn(): proximity lock is not supported. try manually."); manualTurnScreenOn(); } } @ReactMethod public void turnScreenOff() { if (proximityManager.isProximityWakeLockSupported()) { Log.d(TAG, "turnScreenOff(): use proximity lock."); proximityManager.acquireProximityWakeLock(); } else { Log.d(TAG, "turnScreenOff(): proximity lock is not supported. try manually."); manualTurnScreenOff(); } } @ReactMethod public void setKeepScreenOn(final boolean enable) { Log.d(TAG, "setKeepScreenOn() " + enable); UiThreadUtil.runOnUiThread(new Runnable() { public void run() { Activity mCurrentActivity = getCurrentActivity(); if (mCurrentActivity == null) { Log.d(TAG, "ReactContext doesn't hava any Activity attached."); return; } Window window = mCurrentActivity.getWindow(); if (enable) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } }); } @ReactMethod public void setSpeakerphoneOn(final boolean enable) { if (enable != audioManager.isSpeakerphoneOn()) { Log.d(TAG, "setSpeakerphoneOn(): " + enable); audioManager.setSpeakerphoneOn(enable); } } // --- TODO (zxcpoiu): These two api name is really confusing. should be changed. /** * flag: Int * 0: use default action * 1: force speaker on * -1: force speaker off */ @ReactMethod public void setForceSpeakerphoneOn(final int flag) { if (flag < -1 || flag > 1) { return; } Log.d(TAG, "setForceSpeakerphoneOn() flag: " + flag); forceSpeakerOn = flag; // --- will call updateAudioDeviceState() // --- Note: in some devices, it may not contains specified route thus will not be effected. if (flag == 1) { selectAudioDevice(AudioDevice.SPEAKER_PHONE); } else if (flag == -1) { selectAudioDevice(AudioDevice.EARPIECE); // --- use the most common earpiece to force `speaker off` } else { selectAudioDevice(AudioDevice.NONE); // --- NONE will follow default route, the default route of `video` call is speaker. } } // --- TODO (zxcpoiu): Implement api to let user choose audio devices @ReactMethod public void setMicrophoneMute(final boolean enable) { if (enable != audioManager.isMicrophoneMute()) { Log.d(TAG, "setMicrophoneMute(): " + enable); audioManager.setMicrophoneMute(enable); } } /** * This is part of start() process. * ringbackUriType must not empty. empty means do not play. */ @ReactMethod public void startRingback(final String ringbackUriType) { if (ringbackUriType.isEmpty()) { return; } try { Log.d(TAG, "startRingback(): UriType=" + ringbackUriType); if (mRingback != null) { if (mRingback.isPlaying()) { Log.d(TAG, "startRingback(): is already playing"); return; } else { stopRingback(); // --- use brandnew instance } } Uri ringbackUri; Map data = new HashMap<String, Object>(); data.put("name", "mRingback"); if (ringbackUriType.equals("_DTMF_")) { mRingback = new myToneGenerator(myToneGenerator.RINGBACK); mRingback.startPlay(data); return; } else { ringbackUri = getRingbackUri(ringbackUriType); if (ringbackUri == null) { Log.d(TAG, "startRingback(): no available media"); return; } } mRingback = new myMediaPlayer(); data.put("sourceUri", ringbackUri); data.put("setLooping", true); data.put("audioStream", AudioManager.STREAM_VOICE_CALL); /* TODO: for API 21 data.put("audioFlag", AudioAttributes.FLAG_AUDIBILITY_ENFORCED); data.put("audioUsage", AudioAttributes.USAGE_VOICE_COMMUNICATION); // USAGE_VOICE_COMMUNICATION_SIGNALLING ? data.put("audioContentType", AudioAttributes.CONTENT_TYPE_SPEECH); // CONTENT_TYPE_MUSIC ? */ setMediaPlayerEvents((MediaPlayer)mRingback, "mRingback"); mRingback.startPlay(data); } catch(Exception e) { Log.d(TAG, "startRingback() failed"); } } @ReactMethod public void stopRingback() { try { if (mRingback != null) { mRingback.stopPlay(); mRingback = null; } } catch(Exception e) { Log.d(TAG, "stopRingback() failed"); } } /** * This is part of start() process. * busytoneUriType must not empty. empty means do not play. * return false to indicate play tone failed and should be stop() immediately * otherwise, it will stop() after a tone completed. */ public boolean startBusytone(final String busytoneUriType) { if (busytoneUriType.isEmpty()) { return false; } try { Log.d(TAG, "startBusytone(): UriType=" + busytoneUriType); if (mBusytone != null) { if (mBusytone.isPlaying()) { Log.d(TAG, "startBusytone(): is already playing"); return false; } else { stopBusytone(); // --- use brandnew instance } } Uri busytoneUri; Map data = new HashMap<String, Object>(); data.put("name", "mBusytone"); if (busytoneUriType.equals("_DTMF_")) { mBusytone = new myToneGenerator(myToneGenerator.BUSY); mBusytone.startPlay(data); return true; } else { busytoneUri = getBusytoneUri(busytoneUriType); if (busytoneUri == null) { Log.d(TAG, "startBusytone(): no available media"); return false; } } mBusytone = new myMediaPlayer(); data.put("sourceUri", busytoneUri); data.put("setLooping", false); data.put("audioStream", AudioManager.STREAM_VOICE_CALL); /* TODO: for API 21 data.put("audioFlag", AudioAttributes.FLAG_AUDIBILITY_ENFORCED); data.put("audioUsage", AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING); // USAGE_VOICE_COMMUNICATION ? data.put("audioContentType", AudioAttributes.CONTENT_TYPE_SPEECH); */ setMediaPlayerEvents((MediaPlayer)mBusytone, "mBusytone"); mBusytone.startPlay(data); return true; } catch(Exception e) { Log.d(TAG, "startBusytone() failed"); Log.d(TAG, e.getMessage()); return false; } } public void stopBusytone() { try { if (mBusytone != null) { mBusytone.stopPlay(); mBusytone = null; } } catch(Exception e) { Log.d(TAG, "stopBusytone() failed"); } } @ReactMethod public void startRingtone(final String ringtoneUriType, final int seconds) { try { Log.d(TAG, "startRingtone(): UriType=" + ringtoneUriType); if (mRingtone != null) { if (mRingtone.isPlaying()) { Log.d(TAG, "startRingtone(): is already playing"); return; } else { stopRingtone(); // --- use brandnew instance } } //if (!audioManager.isStreamMute(AudioManager.STREAM_RING)) { //if (origRingerMode == AudioManager.RINGER_MODE_NORMAL) { if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) { Log.d(TAG, "startRingtone(): ringer is silent. leave without play."); return; } // --- there is no _DTMF_ option in startRingtone() Uri ringtoneUri = getRingtoneUri(ringtoneUriType); if (ringtoneUri == null) { Log.d(TAG, "startRingtone(): no available media"); return; } if (audioManagerActivated) { stop(); } wakeLockUtils.acquirePartialWakeLock(); storeOriginalAudioSetup(); Map data = new HashMap<String, Object>(); mRingtone = new myMediaPlayer(); data.put("name", "mRingtone"); data.put("sourceUri", ringtoneUri); data.put("setLooping", true); data.put("audioStream", AudioManager.STREAM_RING); /* TODO: for API 21 data.put("audioFlag", 0); data.put("audioUsage", AudioAttributes.USAGE_NOTIFICATION_RINGTONE); // USAGE_NOTIFICATION_COMMUNICATION_REQUEST ? data.put("audioContentType", AudioAttributes.CONTENT_TYPE_MUSIC); */ setMediaPlayerEvents((MediaPlayer) mRingtone, "mRingtone"); mRingtone.startPlay(data); if (seconds > 0) { mRingtoneCountDownHandler = new Handler(); mRingtoneCountDownHandler.postDelayed(new Runnable() { public void run() { try { Log.d(TAG, String.format("mRingtoneCountDownHandler.stopRingtone() timeout after %d seconds", seconds)); stopRingtone(); } catch(Exception e) { Log.d(TAG, "mRingtoneCountDownHandler.stopRingtone() failed."); } } }, seconds * 1000); } } catch(Exception e) { wakeLockUtils.releasePartialWakeLock(); Log.d(TAG, "startRingtone() failed"); } } @ReactMethod public void stopRingtone() { try { if (mRingtone != null) { mRingtone.stopPlay(); mRingtone = null; restoreOriginalAudioSetup(); } if (mRingtoneCountDownHandler != null) { mRingtoneCountDownHandler.removeCallbacksAndMessages(null); mRingtoneCountDownHandler = null; } } catch(Exception e) { Log.d(TAG, "stopRingtone() failed"); } wakeLockUtils.releasePartialWakeLock(); } private void setMediaPlayerEvents(MediaPlayer mp, final String name) { mp.setOnErrorListener(new MediaPlayer.OnErrorListener() { //http://developer.android.com/reference/android/media/MediaPlayer.OnErrorListener.html @Override public boolean onError(MediaPlayer mp, int what, int extra) { Log.d(TAG, String.format("MediaPlayer %s onError(). what: %d, extra: %d", name, what, extra)); //return True if the method handled the error //return False, or not having an OnErrorListener at all, will cause the OnCompletionListener to be called. Get news & tips return true; } }); mp.setOnInfoListener(new MediaPlayer.OnInfoListener() { //http://developer.android.com/reference/android/media/MediaPlayer.OnInfoListener.html @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { Log.d(TAG, String.format("MediaPlayer %s onInfo(). what: %d, extra: %d", name, what, extra)); //return True if the method handled the info //return False, or not having an OnInfoListener at all, will cause the info to be discarded. return true; } }); mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { Log.d(TAG, String.format("MediaPlayer %s onPrepared(), start play, isSpeakerPhoneOn %b", name, audioManager.isSpeakerphoneOn())); if (name.equals("mBusytone")) { audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } else if (name.equals("mRingback")) { audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } else if (name.equals("mRingtone")) { audioManager.setMode(AudioManager.MODE_RINGTONE); } updateAudioRoute(); mp.start(); } }); mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { Log.d(TAG, String.format("MediaPlayer %s onCompletion()", name)); if (name.equals("mBusytone")) { Log.d(TAG, "MyMediaPlayer(): invoke stop()"); stop(); } } }); } // ===== File Uri Start ===== @ReactMethod public void getAudioUriJS(String audioType, String fileType, Promise promise) { Uri result = null; if (audioType.equals("ringback")) { result = getRingbackUri(fileType); } else if (audioType.equals("busytone")) { result = getBusytoneUri(fileType); } else if (audioType.equals("ringtone")) { result = getRingtoneUri(fileType); } try { if (result != null) { promise.resolve(result.toString()); } else { promise.reject("failed"); } } catch (Exception e) { promise.reject("failed"); } } private Uri getRingtoneUri(final String _type) { final String fileBundle = "incallmanager_ringtone"; final String fileBundleExt = "mp3"; final String fileSysWithExt = "media_volume.ogg"; final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock" String type; // --- _type MAY be empty if (_type.equals("_DEFAULT_") || _type.isEmpty()) { //type = fileSysWithExt; return getDefaultUserUri("defaultRingtoneUri"); } else { type = _type; } return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleRingtoneUri", "defaultRingtoneUri"); } private Uri getRingbackUri(final String _type) { final String fileBundle = "incallmanager_ringback"; final String fileBundleExt = "mp3"; final String fileSysWithExt = "media_volume.ogg"; final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock" String type; // --- _type would never be empty here. just in case. if (_type.equals("_DEFAULT_") || _type.isEmpty()) { //type = fileSysWithExt; return getDefaultUserUri("defaultRingbackUri"); } else { type = _type; } return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleRingbackUri", "defaultRingbackUri"); } private Uri getBusytoneUri(final String _type) { final String fileBundle = "incallmanager_busytone"; final String fileBundleExt = "mp3"; final String fileSysWithExt = "LowBattery.ogg"; final String fileSysPath = "/system/media/audio/ui"; // --- every devices all ships with different in ringtone. maybe ui sounds are more "stock" String type; // --- _type would never be empty here. just in case. if (_type.equals("_DEFAULT_") || _type.isEmpty()) { //type = fileSysWithExt; // --- return getDefaultUserUri("defaultBusytoneUri"); } else { type = _type; } return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleBusytoneUri", "defaultBusytoneUri"); } private Uri getAudioUri(final String _type, final String fileBundle, final String fileBundleExt, final String fileSysWithExt, final String fileSysPath, final String uriBundle, final String uriDefault) { String type = _type; if (type.equals("_BUNDLE_")) { if (audioUriMap.get(uriBundle) == null) { int res = 0; ReactContext reactContext = getReactApplicationContext(); if (reactContext != null) { res = reactContext.getResources().getIdentifier(fileBundle, "raw", mPackageName); } else { Log.d(TAG, "getAudioUri() reactContext is null"); } if (res <= 0) { Log.d(TAG, String.format("getAudioUri() %s.%s not found in bundle.", fileBundle, fileBundleExt)); audioUriMap.put(uriBundle, null); //type = fileSysWithExt; return getDefaultUserUri(uriDefault); // --- if specified bundle but not found, use default directlly } else { audioUriMap.put(uriBundle, Uri.parse("android.resource://" + mPackageName + "/" + Integer.toString(res))); //bundleRingtoneUri = Uri.parse("android.resource://" + reactContext.getPackageName() + "/" + R.raw.incallmanager_ringtone); //bundleRingtoneUri = Uri.parse("android.resource://" + reactContext.getPackageName() + "/raw/incallmanager_ringtone"); Log.d(TAG, "getAudioUri() using: " + type); return audioUriMap.get(uriBundle); } } else { Log.d(TAG, "getAudioUri() using: " + type); return audioUriMap.get(uriBundle); } } // --- Check file every time in case user deleted. final String target = fileSysPath + "/" + type; Uri _uri = getSysFileUri(target); if (_uri == null) { Log.d(TAG, "getAudioUri() using user default"); return getDefaultUserUri(uriDefault); } else { Log.d(TAG, "getAudioUri() using internal: " + target); audioUriMap.put(uriDefault, _uri); return _uri; } } private Uri getSysFileUri(final String target) { File file = new File(target); if (file.isFile()) { return Uri.fromFile(file); } return null; } private Uri getDefaultUserUri(final String type) { // except ringtone, it doesn't suppose to be go here. and every android has different files unlike apple; if (type.equals("defaultRingtoneUri")) { return Settings.System.DEFAULT_RINGTONE_URI; } else if (type.equals("defaultRingbackUri")) { return Settings.System.DEFAULT_RINGTONE_URI; } else if (type.equals("defaultBusytoneUri")) { return Settings.System.DEFAULT_NOTIFICATION_URI; // --- DEFAULT_ALARM_ALERT_URI } else { return Settings.System.DEFAULT_NOTIFICATION_URI; } } // ===== File Uri End ===== // ===== Internal Classes Start ===== private class myToneGenerator extends Thread implements MyPlayerInterface { private int toneType; private int toneCategory; private boolean playing = false; private static final int maxWaitTimeMs = 3600000; // 1 hour fairly enough private static final int loadBufferWaitTimeMs = 20; private static final int toneVolume = 100; // The volume of the tone, given in percentage of maximum volume (from 0-100). // --- constant in ToneGenerator all below 100 public static final int BEEP = 101; public static final int BUSY = 102; public static final int CALLEND = 103; public static final int CALLWAITING = 104; public static final int RINGBACK = 105; public static final int SILENT = 106; public int customWaitTimeMs = maxWaitTimeMs; public String caller; myToneGenerator(final int t) { super(); toneCategory = t; } public void setCustomWaitTime(final int ms) { customWaitTimeMs = ms; } @Override public void startPlay(final Map data) { String name = (String) data.get("name"); caller = name; start(); } @Override public boolean isPlaying() { return playing; } @Override public void stopPlay() { synchronized (this) { if (playing) { notify(); } playing = false; } } @Override public void run() { int toneWaitTimeMs; switch (toneCategory) { case SILENT: //toneType = ToneGenerator.TONE_CDMA_SIGNAL_OFF; toneType = ToneGenerator.TONE_CDMA_ANSWER; toneWaitTimeMs = 1000; break; case BUSY: //toneType = ToneGenerator.TONE_SUP_BUSY; //toneType = ToneGenerator.TONE_SUP_CONGESTION; //toneType = ToneGenerator.TONE_SUP_CONGESTION_ABBREV; //toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY; //toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT; toneType = ToneGenerator.TONE_SUP_RADIO_NOTAVAIL; toneWaitTimeMs = 4000; break; case RINGBACK: //toneType = ToneGenerator.TONE_SUP_RINGTONE; toneType = ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK; toneWaitTimeMs = maxWaitTimeMs; // [STOP MANUALLY] break; case CALLEND: toneType = ToneGenerator.TONE_PROP_PROMPT; toneWaitTimeMs = 200; // plays when call ended break; case CALLWAITING: //toneType = ToneGenerator.TONE_CDMA_NETWORK_CALLWAITING; toneType = ToneGenerator.TONE_SUP_CALL_WAITING; toneWaitTimeMs = maxWaitTimeMs; // [STOP MANUALLY] break; case BEEP: //toneType = ToneGenerator.TONE_SUP_PIP; //toneType = ToneGenerator.TONE_CDMA_PIP; //toneType = ToneGenerator.TONE_SUP_RADIO_ACK; //toneType = ToneGenerator.TONE_PROP_BEEP; toneType = ToneGenerator.TONE_PROP_BEEP2; toneWaitTimeMs = 1000; // plays when call ended break; default: // --- use ToneGenerator internal type. Log.d(TAG, "myToneGenerator: use internal tone type: " + toneCategory); toneType = toneCategory; toneWaitTimeMs = customWaitTimeMs; } Log.d(TAG, String.format("myToneGenerator: toneCategory: %d ,toneType: %d, toneWaitTimeMs: %d", toneCategory, toneType, toneWaitTimeMs)); ToneGenerator tg; try { tg = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, toneVolume); } catch (RuntimeException e) { Log.d(TAG, "myToneGenerator: Exception caught while creating ToneGenerator: " + e); tg = null; } if (tg != null) { synchronized (this) { if (!playing) { playing = true; // --- make sure audio routing, or it will be wired when switch suddenly if (caller.equals("mBusytone")) { audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } else if (caller.equals("mRingback")) { audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); } else if (caller.equals("mRingtone")) { audioManager.setMode(AudioManager.MODE_RINGTONE); } InCallManagerModule.this.updateAudioRoute(); tg.startTone(toneType); try { wait(toneWaitTimeMs + loadBufferWaitTimeMs); } catch (InterruptedException e) { Log.d(TAG, "myToneGenerator stopped. toneType: " + toneType); } tg.stopTone(); } playing = false; tg.release(); } } Log.d(TAG, "MyToneGenerator(): play finished. caller=" + caller); if (caller.equals("mBusytone")) { Log.d(TAG, "MyToneGenerator(): invoke stop()"); InCallManagerModule.this.stop(); } } } private class myMediaPlayer extends MediaPlayer implements MyPlayerInterface { //myMediaPlayer() { // super(); //} @Override public void stopPlay() { stop(); reset(); release(); } @Override public void startPlay(final Map data) { try { Uri sourceUri = (Uri) data.get("sourceUri"); boolean setLooping = (Boolean) data.get("setLooping"); int stream = (Integer) data.get("audioStream"); String name = (String) data.get("name"); ReactContext reactContext = getReactApplicationContext(); setDataSource(reactContext, sourceUri); setLooping(setLooping); setAudioStreamType(stream); // is better using STREAM_DTMF for ToneGenerator? /* // TODO: use modern and more explicit audio stream api if (android.os.Build.VERSION.SDK_INT >= 21) { int audioFlag = (Integer) data.get("audioFlag"); int audioUsage = (Integer) data.get("audioUsage"); int audioContentType = (Integer) data.get("audioContentType"); setAudioAttributes( new AudioAttributes.Builder() .setFlags(audioFlag) .setLegacyStreamType(stream) .setUsage(audioUsage) .setContentType(audioContentType) .build() ); } */ // -- will start at onPrepared() event prepareAsync(); } catch (Exception e) { Log.d(TAG, "startPlay() failed"); } } @Override public boolean isPlaying() { return super.isPlaying(); } } // ===== Internal Classes End ===== // ===== Permission Start ===== @ReactMethod public void checkRecordPermission(Promise promise) { Log.d(TAG, "RNInCallManager.checkRecordPermission(): enter"); _checkRecordPermission(); if (recordPermission.equals("unknow")) { Log.d(TAG, "RNInCallManager.checkRecordPermission(): failed"); promise.reject(new Exception("checkRecordPermission failed")); } else { promise.resolve(recordPermission); } } @ReactMethod public void checkCameraPermission(Promise promise) { Log.d(TAG, "RNInCallManager.checkCameraPermission(): enter"); _checkCameraPermission(); if (cameraPermission.equals("unknow")) { Log.d(TAG, "RNInCallManager.checkCameraPermission(): failed"); promise.reject(new Exception("checkCameraPermission failed")); } else { promise.resolve(cameraPermission); } } private void _checkRecordPermission() { recordPermission = _checkPermission(permission.RECORD_AUDIO); Log.d(TAG, String.format("RNInCallManager.checkRecordPermission(): recordPermission=%s", recordPermission)); } private void _checkCameraPermission() { cameraPermission = _checkPermission(permission.CAMERA); Log.d(TAG, String.format("RNInCallManager.checkCameraPermission(): cameraPermission=%s", cameraPermission)); } private String _checkPermission(String targetPermission) { try { ReactContext reactContext = getReactApplicationContext(); if (ContextCompat.checkSelfPermission(reactContext, targetPermission) == PackageManager.PERMISSION_GRANTED) { return "granted"; } else { return "denied"; } } catch (Exception e) { Log.d(TAG, "_checkPermission() catch"); return "denied"; } } @ReactMethod public void requestRecordPermission(Promise promise) { Log.d(TAG, "RNInCallManager.requestRecordPermission(): enter"); _checkRecordPermission(); if (!recordPermission.equals("granted")) { _requestPermission(permission.RECORD_AUDIO, promise); } else { // --- already granted promise.resolve(recordPermission); } } @ReactMethod public void requestCameraPermission(Promise promise) { Log.d(TAG, "RNInCallManager.requestCameraPermission(): enter"); _checkCameraPermission(); if (!cameraPermission.equals("granted")) { _requestPermission(permission.CAMERA, promise); } else { // --- already granted promise.resolve(cameraPermission); } } @ReactMethod public void chooseAudioRoute(String audioRoute, Promise promise) { Log.d(TAG, "RNInCallManager.chooseAudioRoute(): user choose audioDevice = " + audioRoute); if (audioRoute.equals(AudioDevice.EARPIECE.name())) { selectAudioDevice(AudioDevice.EARPIECE); } else if (audioRoute.equals(AudioDevice.SPEAKER_PHONE.name())) { selectAudioDevice(AudioDevice.SPEAKER_PHONE); } else if (audioRoute.equals(AudioDevice.WIRED_HEADSET.name())) { selectAudioDevice(AudioDevice.WIRED_HEADSET); } else if (audioRoute.equals(AudioDevice.BLUETOOTH.name())) { selectAudioDevice(AudioDevice.BLUETOOTH); } promise.resolve(getAudioDeviceStatusMap()); } private void _requestPermission(String targetPermission, Promise promise) { Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { Log.d(TAG, String.format("RNInCallManager._requestPermission(): ReactContext doesn't hava any Activity attached when requesting %s", targetPermission)); promise.reject(new Exception("_requestPermission(): currentActivity is not attached")); return; } int requestPermissionCode = getRandomInteger(1, 65535); while (mRequestPermissionCodePromises.get(requestPermissionCode, null) != null) { requestPermissionCode = getRandomInteger(1, 65535); } mRequestPermissionCodePromises.put(requestPermissionCode, promise); mRequestPermissionCodeTargetPermission.put(requestPermissionCode, targetPermission); /* if (ActivityCompat.shouldShowRequestPermissionRationale(currentActivity, permission.RECORD_AUDIO)) { showMessageOKCancel("You need to allow access to microphone for making call", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityCompat.requestPermissions(currentActivity, new String[] {permission.RECORD_AUDIO}, requestPermissionCode); } }); return; } */ ActivityCompat.requestPermissions(currentActivity, new String[] {targetPermission}, requestPermissionCode); } private static int getRandomInteger(int min, int max) { if (min >= max) { throw new IllegalArgumentException("max must be greater than min"); } Random random = new Random(); return random.nextInt((max - min) + 1) + min; } protected static void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { Log.d(TAG, "RNInCallManager.onRequestPermissionsResult(): enter"); Promise promise = mRequestPermissionCodePromises.get(requestCode, null); String targetPermission = mRequestPermissionCodeTargetPermission.get(requestCode, null); mRequestPermissionCodePromises.delete(requestCode); mRequestPermissionCodeTargetPermission.delete(requestCode); if (promise != null && targetPermission != null) { Map<String, Integer> permissionResultMap = new HashMap<String, Integer>(); for (int i = 0; i < permissions.length; i++) { permissionResultMap.put(permissions[i], grantResults[i]); } if (!permissionResultMap.containsKey(targetPermission)) { Log.wtf(TAG, String.format("RNInCallManager.onRequestPermissionsResult(): requested permission %s but did not appear", targetPermission)); promise.reject(String.format("%s_PERMISSION_NOT_FOUND", targetPermission), String.format("requested permission %s but did not appear", targetPermission)); return; } String _requestPermissionResult = "unknow"; if (permissionResultMap.get(targetPermission) == PackageManager.PERMISSION_GRANTED) { _requestPermissionResult = "granted"; } else { _requestPermissionResult = "denied"; } if (targetPermission.equals(permission.RECORD_AUDIO)) { recordPermission = _requestPermissionResult; } else if (targetPermission.equals(permission.CAMERA)) { cameraPermission = _requestPermissionResult; } promise.resolve(_requestPermissionResult); } else { //super.onRequestPermissionsResult(requestCode, permissions, grantResults); Log.wtf(TAG, "RNInCallManager.onRequestPermissionsResult(): request code not found"); promise.reject("PERMISSION_REQUEST_CODE_NOT_FOUND", "request code not found"); } } // ===== Permission End ===== private void pause() { if (audioManagerActivated) { Log.d(TAG, "pause audioRouteManager"); stopEvents(); } } private void resume() { if (audioManagerActivated) { Log.d(TAG, "resume audioRouteManager"); startEvents(); } } @Override public void onHostResume() { Log.d(TAG, "onResume()"); //resume(); } @Override public void onHostPause() { Log.d(TAG, "onPause()"); //pause(); } @Override public void onHostDestroy() { Log.d(TAG, "onDestroy()"); stopRingtone(); stopRingback(); stopBusytone(); stop(); } private void updateAudioRoute() { if (!automatic) { return; } updateAudioDeviceState(); } // ===== NOTE: below functions is based on appRTC DEMO M64 ===== // /** Changes selection of the currently active audio device. */ private void setAudioDeviceInternal(AudioDevice device) { Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")"); if (!audioDevices.contains(device)) { Log.e(TAG, "specified audio device does not exist"); return; } switch (device) { case SPEAKER_PHONE: setSpeakerphoneOn(true); break; case EARPIECE: setSpeakerphoneOn(false); break; case WIRED_HEADSET: setSpeakerphoneOn(false); break; case BLUETOOTH: setSpeakerphoneOn(false); break; default: Log.e(TAG, "Invalid audio device selection"); break; } selectedAudioDevice = device; } /** * Changes default audio device. * TODO(henrika): add usage of this method in the AppRTCMobile client. */ public void setDefaultAudioDevice(AudioDevice defaultDevice) { switch (defaultDevice) { case SPEAKER_PHONE: defaultAudioDevice = defaultDevice; break; case EARPIECE: if (hasEarpiece()) { defaultAudioDevice = defaultDevice; } else { defaultAudioDevice = AudioDevice.SPEAKER_PHONE; } break; default: Log.e(TAG, "Invalid default audio device selection"); break; } Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); updateAudioDeviceState(); } /** Changes selection of the currently active audio device. */ public void selectAudioDevice(AudioDevice device) { if (device != AudioDevice.NONE && !audioDevices.contains(device)) { Log.e(TAG, "selectAudioDevice() Can not select " + device + " from available " + audioDevices); return; } userSelectedAudioDevice = device; updateAudioDeviceState(); } /** Returns current set of available/selectable audio devices. */ public Set<AudioDevice> getAudioDevices() { return Collections.unmodifiableSet(new HashSet<>(audioDevices)); } /** Returns the currently selected audio device. */ public AudioDevice getSelectedAudioDevice() { return selectedAudioDevice; } /** Helper method for receiver registration. */ private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { getReactApplicationContext().registerReceiver(receiver, filter); } /** Helper method for unregistration of an existing receiver. */ private void unregisterReceiver(final BroadcastReceiver receiver) { final ReactContext reactContext = this.getReactApplicationContext(); if (reactContext != null) { try { reactContext.unregisterReceiver(receiver); } catch (final Exception e) { Log.d(TAG, "unregisterReceiver() failed"); } } else { Log.d(TAG, "unregisterReceiver() reactContext is null"); } } /** Sets the speaker phone mode. */ /* private void setSpeakerphoneOn(boolean on) { boolean wasOn = audioManager.isSpeakerphoneOn(); if (wasOn == on) { return; } audioManager.setSpeakerphoneOn(on); } */ /** Sets the microphone mute state. */ /* private void setMicrophoneMute(boolean on) { boolean wasMuted = audioManager.isMicrophoneMute(); if (wasMuted == on) { return; } audioManager.setMicrophoneMute(on); } */ /** Gets the current earpiece state. */ private boolean hasEarpiece() { return getReactApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); } /** * Checks whether a wired headset is connected or not. * This is not a valid indication that audio playback is actually over * the wired headset as audio routing depends on other conditions. We * only use it as an early indicator (during initialization) of an attached * wired headset. */ @Deprecated private boolean hasWiredHeadset() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return audioManager.isWiredHeadsetOn(); } else { final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); for (AudioDeviceInfo device : devices) { final int type = device.getType(); if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { Log.d(TAG, "hasWiredHeadset: found wired headset"); return true; } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { Log.d(TAG, "hasWiredHeadset: found USB audio device"); return true; } } return false; } } /** * Updates list of possible audio devices and make new device selection. */ public void updateAudioDeviceState() { Log.d(TAG, "--- updateAudioDeviceState: " + "wired headset=" + hasWiredHeadset + ", " + "BT state=" + bluetoothManager.getState()); Log.d(TAG, "Device status: " + "available=" + audioDevices + ", " + "selected=" + selectedAudioDevice + ", " + "user selected=" + userSelectedAudioDevice); // Check if any Bluetooth headset is connected. The internal BT state will // change accordingly. // TODO(henrika): perhaps wrap required state into BT manager. if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) { bluetoothManager.updateDevice(); } // Update the set of available audio devices. Set<AudioDevice> newAudioDevices = new HashSet<>(); // always assume device has speaker phone newAudioDevices.add(AudioDevice.SPEAKER_PHONE); if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { newAudioDevices.add(AudioDevice.BLUETOOTH); } if (hasWiredHeadset) { newAudioDevices.add(AudioDevice.WIRED_HEADSET); } if (hasEarpiece()) { newAudioDevices.add(AudioDevice.EARPIECE); } // --- check whether user selected audio device is available if (userSelectedAudioDevice != null && userSelectedAudioDevice != AudioDevice.NONE && !newAudioDevices.contains(userSelectedAudioDevice)) { userSelectedAudioDevice = AudioDevice.NONE; } // Store state which is set to true if the device list has changed. boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); // Update the existing audio device set. audioDevices = newAudioDevices; AudioDevice newAudioDevice = getPreferredAudioDevice(); // --- stop bluetooth if needed if (selectedAudioDevice == AudioDevice.BLUETOOTH && newAudioDevice != AudioDevice.BLUETOOTH && (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) ) { bluetoothManager.stopScoAudio(); bluetoothManager.updateDevice(); } // --- start bluetooth if needed if (selectedAudioDevice != AudioDevice.BLUETOOTH && newAudioDevice == AudioDevice.BLUETOOTH && bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { // Attempt to start Bluetooth SCO audio (takes a few second to start). if (!bluetoothManager.startScoAudio()) { // Remove BLUETOOTH from list of available devices since SCO failed. audioDevices.remove(AudioDevice.BLUETOOTH); audioDeviceSetUpdated = true; if (userSelectedAudioDevice == AudioDevice.BLUETOOTH) { userSelectedAudioDevice = AudioDevice.NONE; } newAudioDevice = getPreferredAudioDevice(); } } if (newAudioDevice == AudioDevice.BLUETOOTH && bluetoothManager.getState() != AppRTCBluetoothManager.State.SCO_CONNECTED) { newAudioDevice = getPreferredAudioDevice(true); // --- skip bluetooth } // Switch to new device but only if there has been any changes. if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { // Do the required device switch. setAudioDeviceInternal(newAudioDevice); Log.d(TAG, "New device status: " + "available=" + audioDevices + ", " + "selected=" + newAudioDevice); /* if (audioManagerEvents != null) { // Notify a listening client that audio device has been changed. audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); } */ sendEvent("onAudioDeviceChanged", getAudioDeviceStatusMap()); } Log.d(TAG, "--- updateAudioDeviceState done"); } private WritableMap getAudioDeviceStatusMap() { WritableMap data = Arguments.createMap(); String audioDevicesJson = "["; for (AudioDevice s: audioDevices) { audioDevicesJson += "\"" + s.name() + "\","; } // --- strip the last `,` if (audioDevicesJson.length() > 1) { audioDevicesJson = audioDevicesJson.substring(0, audioDevicesJson.length() - 1); } audioDevicesJson += "]"; data.putString("availableAudioDeviceList", audioDevicesJson); data.putString("selectedAudioDevice", (selectedAudioDevice == null) ? "" : selectedAudioDevice.name()); return data; } private AudioDevice getPreferredAudioDevice() { return getPreferredAudioDevice(false); } private AudioDevice getPreferredAudioDevice(boolean skipBluetooth) { final AudioDevice newAudioDevice; if (userSelectedAudioDevice != null && userSelectedAudioDevice != AudioDevice.NONE) { newAudioDevice = userSelectedAudioDevice; } else if (!skipBluetooth && audioDevices.contains(AudioDevice.BLUETOOTH)) { // If a Bluetooth is connected, then it should be used as output audio // device. Note that it is not sufficient that a headset is available; // an active SCO channel must also be up and running. newAudioDevice = AudioDevice.BLUETOOTH; } else if (audioDevices.contains(AudioDevice.WIRED_HEADSET)) { // If a wired headset is connected, but Bluetooth is not, then wired headset is used as // audio device. newAudioDevice = AudioDevice.WIRED_HEADSET; } else if (audioDevices.contains(defaultAudioDevice)) { newAudioDevice = defaultAudioDevice; } else { newAudioDevice = AudioDevice.SPEAKER_PHONE; } return newAudioDevice; } }