|  | @@ -0,0 +1,305 @@
 | 
		
	
		
			
			|  | 1 | +package org.jitsi.meet.audiomode;
 | 
		
	
		
			
			|  | 2 | +
 | 
		
	
		
			
			|  | 3 | +import android.annotation.TargetApi;
 | 
		
	
		
			
			|  | 4 | +import android.content.BroadcastReceiver;
 | 
		
	
		
			
			|  | 5 | +import android.content.Context;
 | 
		
	
		
			
			|  | 6 | +import android.content.Intent;
 | 
		
	
		
			
			|  | 7 | +import android.content.IntentFilter;
 | 
		
	
		
			
			|  | 8 | +import android.media.AudioDeviceInfo;
 | 
		
	
		
			
			|  | 9 | +import android.media.AudioManager;
 | 
		
	
		
			
			|  | 10 | +import android.os.Build;
 | 
		
	
		
			
			|  | 11 | +import android.os.Handler;
 | 
		
	
		
			
			|  | 12 | +import android.os.Looper;
 | 
		
	
		
			
			|  | 13 | +import android.util.Log;
 | 
		
	
		
			
			|  | 14 | +
 | 
		
	
		
			
			|  | 15 | +import com.facebook.react.bridge.Promise;
 | 
		
	
		
			
			|  | 16 | +import com.facebook.react.bridge.ReactApplicationContext;
 | 
		
	
		
			
			|  | 17 | +import com.facebook.react.bridge.ReactContext;
 | 
		
	
		
			
			|  | 18 | +import com.facebook.react.bridge.ReactContextBaseJavaModule;
 | 
		
	
		
			
			|  | 19 | +import com.facebook.react.bridge.ReactMethod;
 | 
		
	
		
			
			|  | 20 | +
 | 
		
	
		
			
			|  | 21 | +import java.util.HashMap;
 | 
		
	
		
			
			|  | 22 | +import java.util.List;
 | 
		
	
		
			
			|  | 23 | +import java.util.Map;
 | 
		
	
		
			
			|  | 24 | +
 | 
		
	
		
			
			|  | 25 | +
 | 
		
	
		
			
			|  | 26 | +/**
 | 
		
	
		
			
			|  | 27 | + * Module implementing a simple API to select the appropriate audio device for a conference call.
 | 
		
	
		
			
			|  | 28 | + *
 | 
		
	
		
			
			|  | 29 | + * Audio calls should use <tt>AudioModeModule.AUDIO_CALL</tt>, which uses the builtin earpiece,
 | 
		
	
		
			
			|  | 30 | + * wired headset or bluetooth headset. The builtin earpiece is the default audio device.
 | 
		
	
		
			
			|  | 31 | + *
 | 
		
	
		
			
			|  | 32 | + * Video calls should should use <tt>AudioModeModule.VIDEO_CALL</tt>, which uses the builtin
 | 
		
	
		
			
			|  | 33 | + * speaker, earpiece, wired headset or bluetooth headset. The builtin speaker is the default
 | 
		
	
		
			
			|  | 34 | + * audio device.
 | 
		
	
		
			
			|  | 35 | + *
 | 
		
	
		
			
			|  | 36 | + * Before a call has started and after it has ended the <tt>AudioModeModule.DEFAULT</tt> mode
 | 
		
	
		
			
			|  | 37 | + * should be used.
 | 
		
	
		
			
			|  | 38 | + */
 | 
		
	
		
			
			|  | 39 | +public class AudioModeModule extends ReactContextBaseJavaModule {
 | 
		
	
		
			
			|  | 40 | +    /**
 | 
		
	
		
			
			|  | 41 | +     * Constants representing the audio mode.
 | 
		
	
		
			
			|  | 42 | +     * - DEFAULT: Used before and after every call.  It represents the default audio routing scheme.
 | 
		
	
		
			
			|  | 43 | +     * - AUDIO_CALL: Used for audio only calls.  It will use the earpiece by default, unless a
 | 
		
	
		
			
			|  | 44 | +     *   wired or Bluetooth headset is connected.
 | 
		
	
		
			
			|  | 45 | +     * - VIDEO_CALL: Used for video calls.  It will use the speaker by default, unless a wired or
 | 
		
	
		
			
			|  | 46 | +     *   Bluetooth headset is connected.
 | 
		
	
		
			
			|  | 47 | +     */
 | 
		
	
		
			
			|  | 48 | +    private static final int DEFAULT    = 0;
 | 
		
	
		
			
			|  | 49 | +    private static final int AUDIO_CALL = 1;
 | 
		
	
		
			
			|  | 50 | +    private static final int VIDEO_CALL = 2;
 | 
		
	
		
			
			|  | 51 | +
 | 
		
	
		
			
			|  | 52 | +    /**
 | 
		
	
		
			
			|  | 53 | +     * React Native module name.
 | 
		
	
		
			
			|  | 54 | +     */
 | 
		
	
		
			
			|  | 55 | +    private static final String MODULE_NAME = "AudioMode";
 | 
		
	
		
			
			|  | 56 | +
 | 
		
	
		
			
			|  | 57 | +    /**
 | 
		
	
		
			
			|  | 58 | +     * Tag used when logging messages.
 | 
		
	
		
			
			|  | 59 | +     */
 | 
		
	
		
			
			|  | 60 | +    static final String TAG = MODULE_NAME;
 | 
		
	
		
			
			|  | 61 | +
 | 
		
	
		
			
			|  | 62 | +    /**
 | 
		
	
		
			
			|  | 63 | +     * Audio mode currently in use.
 | 
		
	
		
			
			|  | 64 | +     */
 | 
		
	
		
			
			|  | 65 | +    private int mode = -1;
 | 
		
	
		
			
			|  | 66 | +
 | 
		
	
		
			
			|  | 67 | +    /**
 | 
		
	
		
			
			|  | 68 | +     * {@link AudioManager} instance used to interact with the Android audio subsystem.
 | 
		
	
		
			
			|  | 69 | +     */
 | 
		
	
		
			
			|  | 70 | +    private final AudioManager audioManager;
 | 
		
	
		
			
			|  | 71 | +
 | 
		
	
		
			
			|  | 72 | +    /**
 | 
		
	
		
			
			|  | 73 | +     * {@link Handler} for running all operations on the main thread.
 | 
		
	
		
			
			|  | 74 | +     */
 | 
		
	
		
			
			|  | 75 | +    private final Handler mainThreadHandler;
 | 
		
	
		
			
			|  | 76 | +
 | 
		
	
		
			
			|  | 77 | +    /**
 | 
		
	
		
			
			|  | 78 | +     * {@link Runnable} for running update operation on the main thread.
 | 
		
	
		
			
			|  | 79 | +     */
 | 
		
	
		
			
			|  | 80 | +    private final Runnable mainThreadRunner;
 | 
		
	
		
			
			|  | 81 | +
 | 
		
	
		
			
			|  | 82 | +    /**
 | 
		
	
		
			
			|  | 83 | +     * {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in old (< M)
 | 
		
	
		
			
			|  | 84 | +     * Android versions.
 | 
		
	
		
			
			|  | 85 | +     */
 | 
		
	
		
			
			|  | 86 | +    private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
 | 
		
	
		
			
			|  | 87 | +
 | 
		
	
		
			
			|  | 88 | +    /**
 | 
		
	
		
			
			|  | 89 | +     *
 | 
		
	
		
			
			|  | 90 | +     */
 | 
		
	
		
			
			|  | 91 | +    private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ?
 | 
		
	
		
			
			|  | 92 | +            AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG;
 | 
		
	
		
			
			|  | 93 | +
 | 
		
	
		
			
			|  | 94 | +    /**
 | 
		
	
		
			
			|  | 95 | +     * Initializes a new module instance. There shall be a single instance of this module throughout
 | 
		
	
		
			
			|  | 96 | +     * the lifetime of the application.
 | 
		
	
		
			
			|  | 97 | +     *
 | 
		
	
		
			
			|  | 98 | +     * @param reactContext the {@link ReactApplicationContext} where this module is created.
 | 
		
	
		
			
			|  | 99 | +     */
 | 
		
	
		
			
			|  | 100 | +    public AudioModeModule(ReactApplicationContext reactContext) {
 | 
		
	
		
			
			|  | 101 | +        super(reactContext);
 | 
		
	
		
			
			|  | 102 | +
 | 
		
	
		
			
			|  | 103 | +        audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE));
 | 
		
	
		
			
			|  | 104 | +        mainThreadHandler = new Handler(Looper.getMainLooper());
 | 
		
	
		
			
			|  | 105 | +        mainThreadRunner = new Runnable() {
 | 
		
	
		
			
			|  | 106 | +            @Override
 | 
		
	
		
			
			|  | 107 | +            public void run() {
 | 
		
	
		
			
			|  | 108 | +                if (mode != -1) {
 | 
		
	
		
			
			|  | 109 | +                    updateAudioRoute(mode);
 | 
		
	
		
			
			|  | 110 | +                }
 | 
		
	
		
			
			|  | 111 | +            }
 | 
		
	
		
			
			|  | 112 | +        };
 | 
		
	
		
			
			|  | 113 | +
 | 
		
	
		
			
			|  | 114 | +        // Setup runtime device change detection
 | 
		
	
		
			
			|  | 115 | +        setupAudioRouteChangeDetection();
 | 
		
	
		
			
			|  | 116 | +    }
 | 
		
	
		
			
			|  | 117 | +
 | 
		
	
		
			
			|  | 118 | +    /**
 | 
		
	
		
			
			|  | 119 | +     * Gets the name for this module, to be used in the React Native bridge.
 | 
		
	
		
			
			|  | 120 | +     *
 | 
		
	
		
			
			|  | 121 | +     * @return a string with the module name.
 | 
		
	
		
			
			|  | 122 | +     */
 | 
		
	
		
			
			|  | 123 | +    @Override
 | 
		
	
		
			
			|  | 124 | +    public String getName() {
 | 
		
	
		
			
			|  | 125 | +        return MODULE_NAME;
 | 
		
	
		
			
			|  | 126 | +    }
 | 
		
	
		
			
			|  | 127 | +
 | 
		
	
		
			
			|  | 128 | +    /**
 | 
		
	
		
			
			|  | 129 | +     * Gets a mapping with the constants this module is exporting.
 | 
		
	
		
			
			|  | 130 | +     *
 | 
		
	
		
			
			|  | 131 | +     * @return a {@link Map} mapping the constants to be exported with their values.
 | 
		
	
		
			
			|  | 132 | +     */
 | 
		
	
		
			
			|  | 133 | +    @Override
 | 
		
	
		
			
			|  | 134 | +    public Map<String, Object> getConstants() {
 | 
		
	
		
			
			|  | 135 | +        Map<String, Object> constants = new HashMap<>();
 | 
		
	
		
			
			|  | 136 | +        constants.put("DEFAULT", DEFAULT);
 | 
		
	
		
			
			|  | 137 | +        constants.put("AUDIO_CALL", AUDIO_CALL);
 | 
		
	
		
			
			|  | 138 | +        constants.put("VIDEO_CALL", VIDEO_CALL);
 | 
		
	
		
			
			|  | 139 | +        return constants;
 | 
		
	
		
			
			|  | 140 | +    }
 | 
		
	
		
			
			|  | 141 | +
 | 
		
	
		
			
			|  | 142 | +    /**
 | 
		
	
		
			
			|  | 143 | +     * Updates the audio route for the given mode.
 | 
		
	
		
			
			|  | 144 | +     *
 | 
		
	
		
			
			|  | 145 | +     * @param mode the audio mode to be used when computing the audio route.
 | 
		
	
		
			
			|  | 146 | +     * @return true if the audio route was updated successfully, false otherwise.
 | 
		
	
		
			
			|  | 147 | +     */
 | 
		
	
		
			
			|  | 148 | +    private boolean updateAudioRoute(int mode) {
 | 
		
	
		
			
			|  | 149 | +        Log.d(TAG, "Update audio route for mode: " + mode);
 | 
		
	
		
			
			|  | 150 | +
 | 
		
	
		
			
			|  | 151 | +        if (mode == DEFAULT) {
 | 
		
	
		
			
			|  | 152 | +            audioManager.setMode(AudioManager.MODE_NORMAL);
 | 
		
	
		
			
			|  | 153 | +            audioManager.abandonAudioFocus(null);
 | 
		
	
		
			
			|  | 154 | +            audioManager.setSpeakerphoneOn(false);
 | 
		
	
		
			
			|  | 155 | +            audioManager.setMicrophoneMute(true);
 | 
		
	
		
			
			|  | 156 | +            setBluetoothAudioRoute(false);
 | 
		
	
		
			
			|  | 157 | +
 | 
		
	
		
			
			|  | 158 | +            return true;
 | 
		
	
		
			
			|  | 159 | +        }
 | 
		
	
		
			
			|  | 160 | +
 | 
		
	
		
			
			|  | 161 | +        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
 | 
		
	
		
			
			|  | 162 | +        audioManager.setMicrophoneMute(false);
 | 
		
	
		
			
			|  | 163 | +
 | 
		
	
		
			
			|  | 164 | +        if (audioManager.requestAudioFocus(
 | 
		
	
		
			
			|  | 165 | +                    null,
 | 
		
	
		
			
			|  | 166 | +                    AudioManager.STREAM_VOICE_CALL,
 | 
		
	
		
			
			|  | 167 | +                    AudioManager.AUDIOFOCUS_GAIN)
 | 
		
	
		
			
			|  | 168 | +                == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
 | 
		
	
		
			
			|  | 169 | +            Log.d(TAG, "Audio focus request failed");
 | 
		
	
		
			
			|  | 170 | +            return false;
 | 
		
	
		
			
			|  | 171 | +        }
 | 
		
	
		
			
			|  | 172 | +
 | 
		
	
		
			
			|  | 173 | +        boolean useSpeaker = (mode == VIDEO_CALL);
 | 
		
	
		
			
			|  | 174 | +
 | 
		
	
		
			
			|  | 175 | +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
		
	
		
			
			|  | 176 | +            // On Android >= M we use the AudioDeviceCallback API, so turn on Bluetooth SCO
 | 
		
	
		
			
			|  | 177 | +            // from the start.
 | 
		
	
		
			
			|  | 178 | +            if (audioManager.isBluetoothScoAvailableOffCall()) {
 | 
		
	
		
			
			|  | 179 | +                audioManager.startBluetoothSco();
 | 
		
	
		
			
			|  | 180 | +            }
 | 
		
	
		
			
			|  | 181 | +        } else {
 | 
		
	
		
			
			|  | 182 | +            // On older Android versions we must set the Bluetooth route manually. Also disable
 | 
		
	
		
			
			|  | 183 | +            // the speaker in that case.
 | 
		
	
		
			
			|  | 184 | +            setBluetoothAudioRoute(bluetoothHeadsetMonitor.isHeadsetAvailable());
 | 
		
	
		
			
			|  | 185 | +            if (bluetoothHeadsetMonitor.isHeadsetAvailable()) {
 | 
		
	
		
			
			|  | 186 | +                useSpeaker = false;
 | 
		
	
		
			
			|  | 187 | +            }
 | 
		
	
		
			
			|  | 188 | +        }
 | 
		
	
		
			
			|  | 189 | +
 | 
		
	
		
			
			|  | 190 | +        // XXX: isWiredHeadsetOn is not deprecated when used just for knowing if there is a wired
 | 
		
	
		
			
			|  | 191 | +        // headset connected, regardless of audio being routed to it.
 | 
		
	
		
			
			|  | 192 | +        audioManager.setSpeakerphoneOn(useSpeaker &&
 | 
		
	
		
			
			|  | 193 | +                !(audioManager.isWiredHeadsetOn() || audioManager.isBluetoothScoOn()));
 | 
		
	
		
			
			|  | 194 | +
 | 
		
	
		
			
			|  | 195 | +        return true;
 | 
		
	
		
			
			|  | 196 | +    }
 | 
		
	
		
			
			|  | 197 | +
 | 
		
	
		
			
			|  | 198 | +    /**
 | 
		
	
		
			
			|  | 199 | +     * Public method to set the current audio mode.
 | 
		
	
		
			
			|  | 200 | +     *
 | 
		
	
		
			
			|  | 201 | +     * @param mode the desired audio mode.
 | 
		
	
		
			
			|  | 202 | +     * @param promise a {@link Promise} which will be resolved if the audio mode could be updated
 | 
		
	
		
			
			|  | 203 | +     *                successfully, and it will be rejected otherwise.
 | 
		
	
		
			
			|  | 204 | +     */
 | 
		
	
		
			
			|  | 205 | +    @ReactMethod
 | 
		
	
		
			
			|  | 206 | +    public void setMode(final int mode, final Promise promise) {
 | 
		
	
		
			
			|  | 207 | +        if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) {
 | 
		
	
		
			
			|  | 208 | +            promise.reject("setMode", "Invalid audio mode " + mode);
 | 
		
	
		
			
			|  | 209 | +            return;
 | 
		
	
		
			
			|  | 210 | +        }
 | 
		
	
		
			
			|  | 211 | +
 | 
		
	
		
			
			|  | 212 | +        Runnable r = new Runnable() {
 | 
		
	
		
			
			|  | 213 | +            @Override
 | 
		
	
		
			
			|  | 214 | +            public void run() {
 | 
		
	
		
			
			|  | 215 | +                if (updateAudioRoute(mode)) {
 | 
		
	
		
			
			|  | 216 | +                    AudioModeModule.this.mode = mode;
 | 
		
	
		
			
			|  | 217 | +                    promise.resolve(null);
 | 
		
	
		
			
			|  | 218 | +                } else {
 | 
		
	
		
			
			|  | 219 | +                    promise.reject("setMode", "Failed to set the requested audio mode");
 | 
		
	
		
			
			|  | 220 | +                }
 | 
		
	
		
			
			|  | 221 | +            }
 | 
		
	
		
			
			|  | 222 | +        };
 | 
		
	
		
			
			|  | 223 | +        mainThreadHandler.post(r);
 | 
		
	
		
			
			|  | 224 | +    }
 | 
		
	
		
			
			|  | 225 | +
 | 
		
	
		
			
			|  | 226 | +    /**
 | 
		
	
		
			
			|  | 227 | +     * Setup the audio route change detection mechanism. We use the
 | 
		
	
		
			
			|  | 228 | +     * {@link android.media.AudioDeviceCallback} API on Android >= 23 only.
 | 
		
	
		
			
			|  | 229 | +     */
 | 
		
	
		
			
			|  | 230 | +    private void setupAudioRouteChangeDetection() {
 | 
		
	
		
			
			|  | 231 | +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
		
	
		
			
			|  | 232 | +            setupAudioRouteChangeDetectionM();
 | 
		
	
		
			
			|  | 233 | +        } else {
 | 
		
	
		
			
			|  | 234 | +            setupAudioRouteChangeDetectionOld();
 | 
		
	
		
			
			|  | 235 | +        }
 | 
		
	
		
			
			|  | 236 | +    }
 | 
		
	
		
			
			|  | 237 | +
 | 
		
	
		
			
			|  | 238 | +    /**
 | 
		
	
		
			
			|  | 239 | +     * Audio route change detection mechanism for Android API >= 23.
 | 
		
	
		
			
			|  | 240 | +     */
 | 
		
	
		
			
			|  | 241 | +    @TargetApi(Build.VERSION_CODES.M)
 | 
		
	
		
			
			|  | 242 | +    private void setupAudioRouteChangeDetectionM() {
 | 
		
	
		
			
			|  | 243 | +        android.media.AudioDeviceCallback audioDeviceCallback =
 | 
		
	
		
			
			|  | 244 | +                new android.media.AudioDeviceCallback() {
 | 
		
	
		
			
			|  | 245 | +                    @Override
 | 
		
	
		
			
			|  | 246 | +                    public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
 | 
		
	
		
			
			|  | 247 | +                        Log.d(TAG, "Audio devices added");
 | 
		
	
		
			
			|  | 248 | +                        onAudioDeviceChange();
 | 
		
	
		
			
			|  | 249 | +                    }
 | 
		
	
		
			
			|  | 250 | +
 | 
		
	
		
			
			|  | 251 | +                    @Override
 | 
		
	
		
			
			|  | 252 | +                    public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
 | 
		
	
		
			
			|  | 253 | +                        Log.d(TAG, "Audio devices removed");
 | 
		
	
		
			
			|  | 254 | +                        onAudioDeviceChange();
 | 
		
	
		
			
			|  | 255 | +                    }
 | 
		
	
		
			
			|  | 256 | +                };
 | 
		
	
		
			
			|  | 257 | +
 | 
		
	
		
			
			|  | 258 | +        audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
 | 
		
	
		
			
			|  | 259 | +    }
 | 
		
	
		
			
			|  | 260 | +
 | 
		
	
		
			
			|  | 261 | +    /**
 | 
		
	
		
			
			|  | 262 | +     * Audio route change detection mechanism for Android API < 23.
 | 
		
	
		
			
			|  | 263 | +     */
 | 
		
	
		
			
			|  | 264 | +    private void setupAudioRouteChangeDetectionOld() {
 | 
		
	
		
			
			|  | 265 | +        ReactContext reactContext = getReactApplicationContext();
 | 
		
	
		
			
			|  | 266 | +
 | 
		
	
		
			
			|  | 267 | +        // Detect changes in wired headset connections
 | 
		
	
		
			
			|  | 268 | +        IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
 | 
		
	
		
			
			|  | 269 | +        BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
 | 
		
	
		
			
			|  | 270 | +            @Override
 | 
		
	
		
			
			|  | 271 | +            public void onReceive(Context context, Intent intent) {
 | 
		
	
		
			
			|  | 272 | +                Log.d(TAG, "Wired headset added / removed");
 | 
		
	
		
			
			|  | 273 | +                onAudioDeviceChange();
 | 
		
	
		
			
			|  | 274 | +            }
 | 
		
	
		
			
			|  | 275 | +        };
 | 
		
	
		
			
			|  | 276 | +        reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
 | 
		
	
		
			
			|  | 277 | +
 | 
		
	
		
			
			|  | 278 | +        // Detect Bluetooth device changes
 | 
		
	
		
			
			|  | 279 | +        bluetoothHeadsetMonitor =
 | 
		
	
		
			
			|  | 280 | +                new BluetoothHeadsetMonitor(this, this.getReactApplicationContext());
 | 
		
	
		
			
			|  | 281 | +        bluetoothHeadsetMonitor.start();
 | 
		
	
		
			
			|  | 282 | +    }
 | 
		
	
		
			
			|  | 283 | +
 | 
		
	
		
			
			|  | 284 | +    /**
 | 
		
	
		
			
			|  | 285 | +     * Helper method to set the output route to a Bluetooth device.
 | 
		
	
		
			
			|  | 286 | +     * @param enabled true if Bluetooth should use used, false otherwise.
 | 
		
	
		
			
			|  | 287 | +     */
 | 
		
	
		
			
			|  | 288 | +    private void setBluetoothAudioRoute(boolean enabled) {
 | 
		
	
		
			
			|  | 289 | +        if (enabled) {
 | 
		
	
		
			
			|  | 290 | +            audioManager.startBluetoothSco();
 | 
		
	
		
			
			|  | 291 | +            audioManager.setBluetoothScoOn(true);
 | 
		
	
		
			
			|  | 292 | +        } else {
 | 
		
	
		
			
			|  | 293 | +            audioManager.setBluetoothScoOn(false);
 | 
		
	
		
			
			|  | 294 | +            audioManager.stopBluetoothSco();
 | 
		
	
		
			
			|  | 295 | +        }
 | 
		
	
		
			
			|  | 296 | +    }
 | 
		
	
		
			
			|  | 297 | +
 | 
		
	
		
			
			|  | 298 | +    /**
 | 
		
	
		
			
			|  | 299 | +     * Helper method to trigger an audio route update when devices change.  It makes sure the
 | 
		
	
		
			
			|  | 300 | +     * operation is performed on the main thread.
 | 
		
	
		
			
			|  | 301 | +     */
 | 
		
	
		
			
			|  | 302 | +    void onAudioDeviceChange() {
 | 
		
	
		
			
			|  | 303 | +        mainThreadHandler.post(mainThreadRunner);
 | 
		
	
		
			
			|  | 304 | +    }
 | 
		
	
		
			
			|  | 305 | +}
 |