mirror of https://github.com/jitsi/jitsi-meet
Due to the difference in nature, the iOS and Android implementations are completely different: iOS: MPVolumeView is used, which allows us to place a button which will launch a native route picker provided by iOS itself. This view is different depending on the iOS version, with the iOS 11 version being more complete. Android: A completely custom component is used, which displays a bottom sheet with the device categories, not devices individually. This is akin to the sheet in the builtin dialer.pull/2139/head
parent
8198e52b93
commit
f973a695d8
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,62 @@ |
||||
/* |
||||
* Copyright @ 2017-present Atlassian Pty Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
#import <React/RCTUIManager.h> |
||||
#import <React/RCTViewManager.h> |
||||
|
||||
@import MediaPlayer; |
||||
|
||||
|
||||
@interface MPVolumeViewManager : RCTViewManager |
||||
@end |
||||
|
||||
@implementation MPVolumeViewManager |
||||
|
||||
RCT_EXPORT_MODULE() |
||||
|
||||
- (UIView *)view { |
||||
MPVolumeView *volumeView = [[MPVolumeView alloc] init]; |
||||
volumeView.showsRouteButton = YES; |
||||
volumeView.showsVolumeSlider = NO; |
||||
|
||||
return (UIView *) volumeView; |
||||
} |
||||
|
||||
RCT_EXPORT_METHOD(show:(nonnull NSNumber *)reactTag) { |
||||
[self.bridge.uiManager addUIBlock:^( |
||||
__unused RCTUIManager *uiManager, |
||||
NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
||||
id view = viewRegistry[reactTag]; |
||||
if (![view isKindOfClass:[MPVolumeView class]]) { |
||||
RCTLogError(@"Invalid view returned from registry, expecting \ |
||||
MPVolumeView, got: %@", view); |
||||
} else { |
||||
// Simulate a click |
||||
UIButton *btn = nil; |
||||
for (UIView *buttonView in ((UIView *) view).subviews) { |
||||
if ([buttonView isKindOfClass:[UIButton class]]) { |
||||
btn = (UIButton *) buttonView; |
||||
break; |
||||
} |
||||
} |
||||
if (btn != nil) { |
||||
[btn sendActionsForControlEvents:UIControlEventTouchUpInside]; |
||||
} |
||||
} |
||||
}]; |
||||
} |
||||
|
||||
@end |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,178 @@ |
||||
// @flow
|
||||
|
||||
import _ from 'lodash'; |
||||
import React, { Component } from 'react'; |
||||
import { NativeModules } from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { hideDialog, SimpleBottomSheet } from '../../../base/dialog'; |
||||
|
||||
const AudioMode = NativeModules.AudioMode; |
||||
|
||||
/** |
||||
* Maps each device type to a display name and icon. |
||||
* TODO: internationalization. |
||||
*/ |
||||
const deviceInfoMap = { |
||||
BLUETOOTH: { |
||||
iconName: 'bluetooth', |
||||
text: 'Bluetooth', |
||||
type: 'BLUETOOTH' |
||||
}, |
||||
EARPIECE: { |
||||
iconName: 'phone-talk', |
||||
text: 'Phone', |
||||
type: 'EARPIECE' |
||||
}, |
||||
HEADPHONES: { |
||||
iconName: 'headset', |
||||
text: 'Headphones', |
||||
type: 'HEADPHONES' |
||||
}, |
||||
SPEAKER: { |
||||
iconName: 'volume', |
||||
text: 'Speaker', |
||||
type: 'SPEAKER' |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Variable to hold the reference to the exported component. This dialog is only |
||||
* exported if the {@code AudioMode} module has the capability to get / set |
||||
* audio devices. |
||||
*/ |
||||
let DialogType; |
||||
|
||||
/** |
||||
* {@code PasswordRequiredPrompt}'s React {@code Component} prop types. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Used for hiding the dialog when the selection was completed. |
||||
*/ |
||||
dispatch: Function |
||||
}; |
||||
|
||||
type State = { |
||||
|
||||
/** |
||||
* Array of available devices. |
||||
*/ |
||||
devices: Array<string> |
||||
}; |
||||
|
||||
/** |
||||
* Implements a React {@code Component} which prompts the user when a password |
||||
* is required to join a conference. |
||||
*/ |
||||
class AudioRoutePickerDialog extends Component<Props, State> { |
||||
state = { |
||||
// Available audio devices, it will be set in componentWillMount.
|
||||
devices: [] |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code PasswordRequiredPrompt} instance. |
||||
* |
||||
* @param {Props} props - The read-only React {@code Component} props with |
||||
* which the new instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Initializes the device list by querying the {@code AudioMode} module. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillMount() { |
||||
AudioMode.getAudioDevices().then(({ devices, selected }) => { |
||||
const audioDevices = []; |
||||
|
||||
if (devices) { |
||||
for (const device of devices) { |
||||
const info = deviceInfoMap[device]; |
||||
|
||||
if (info) { |
||||
info.selected = device === selected; |
||||
audioDevices.push(info); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (audioDevices) { |
||||
// Make sure devices is alphabetically sorted
|
||||
this.setState({ devices: _.sortBy(audioDevices, 'text') }); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Dispatches a redux action to hide this sheet. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_hide() { |
||||
this.props.dispatch(hideDialog(DialogType)); |
||||
} |
||||
|
||||
_onCancel: () => void; |
||||
|
||||
/** |
||||
* Cancels the dialog by hiding it. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onCancel() { |
||||
this._hide(); |
||||
} |
||||
|
||||
_onSubmit: (?Object) => void; |
||||
|
||||
/** |
||||
* Handles the selection of a device on the sheet. The selected device will |
||||
* be used by {@code AudioMode}. |
||||
* |
||||
* @param {Object} device - Object representing the selected device. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSubmit(device) { |
||||
this._hide(); |
||||
AudioMode.setAudioDevice(device.type); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
if (!this.state.devices.length) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<SimpleBottomSheet |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onSubmit } |
||||
options = { this.state.devices } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
// Only export the dialog if we have support for getting / setting audio devices
|
||||
// in AudioMode.
|
||||
if (AudioMode.getAudioDevices && AudioMode.setAudioDevice) { |
||||
DialogType = connect()(AudioRoutePickerDialog); |
||||
} |
||||
|
||||
export default DialogType; |
@ -0,0 +1,3 @@ |
||||
export { |
||||
default as AudioRoutePickerDialog |
||||
} from './AudioRoutePickerDialog'; |
@ -1 +1,3 @@ |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
|
@ -0,0 +1,160 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { |
||||
findNodeHandle, |
||||
requireNativeComponent, |
||||
NativeModules, |
||||
View |
||||
} from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { openDialog } from '../../base/dialog'; |
||||
import { AudioRoutePickerDialog } from '../../mobile/audio-mode'; |
||||
|
||||
import ToolbarButton from './ToolbarButton'; |
||||
|
||||
/** |
||||
* Define the {@code MPVolumeView} React component. It will only be available |
||||
* on iOS. |
||||
*/ |
||||
let MPVolumeView; |
||||
|
||||
if (NativeModules.MPVolumeViewManager) { |
||||
MPVolumeView = requireNativeComponent('MPVolumeView', null); |
||||
} |
||||
|
||||
/** |
||||
* Style required to hide the {@code MPVolumeView} view, since it's displayed |
||||
* programmatically. |
||||
*/ |
||||
const HIDE_VIEW_STYLE = { display: 'none' }; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Used to show the {@code AudioRoutePickerDialog}. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The name of the Icon of this {@code AudioRouteButton}. |
||||
*/ |
||||
iconName: string, |
||||
|
||||
/** |
||||
* The style of the Icon of this {@code AudioRouteButton}. |
||||
*/ |
||||
iconStyle: Object, |
||||
|
||||
/** |
||||
* {@code AudioRouteButton} styles. |
||||
*/ |
||||
style: Array<*> | Object, |
||||
|
||||
/** |
||||
* The color underlying the button. |
||||
*/ |
||||
underlayColor: string |
||||
}; |
||||
|
||||
/** |
||||
* A toolbar button which triggers an audio route picker when pressed. |
||||
*/ |
||||
class AudioRouteButton extends Component<Props> { |
||||
_volumeComponent: ?Object; |
||||
|
||||
/** |
||||
* Indicates if there is support for audio device selection via this button. |
||||
* |
||||
* @returns {boolean} - True if audio device selection is supported, false |
||||
* otherwise. |
||||
*/ |
||||
static supported() { |
||||
return Boolean(MPVolumeView || AudioRoutePickerDialog); |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new {@code AudioRouteButton} instance. |
||||
* |
||||
* @param {Object} props - The React {@code Component} props to initialize |
||||
* the new {@code AudioRouteButton} instance with. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
/** |
||||
* The internal reference to the React {@code MPVolumeView} for |
||||
* showing the volume control view. |
||||
* |
||||
* @private |
||||
* @type {ReactComponent} |
||||
*/ |
||||
this._volumeComponent = null; |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClick = this._onClick.bind(this); |
||||
this._setVolumeComponent = this._setVolumeComponent.bind(this); |
||||
} |
||||
|
||||
_onClick: () => void; |
||||
|
||||
/** |
||||
* Handles clicking/pressing this {@code AudioRouteButton} by showing an |
||||
* audio route picker. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
if (MPVolumeView) { |
||||
const handle = findNodeHandle(this._volumeComponent); |
||||
|
||||
NativeModules.MPVolumeViewManager.show(handle); |
||||
} else if (AudioRoutePickerDialog) { |
||||
this.props.dispatch(openDialog(AudioRoutePickerDialog)); |
||||
} |
||||
} |
||||
|
||||
_setVolumeComponent: (?Object) => void; |
||||
|
||||
/** |
||||
* Sets the internal reference to the React Component wrapping the |
||||
* {@code MPVolumeView} component. |
||||
* |
||||
* @param {ReactComponent} component - React Component. |
||||
* @returns {void} |
||||
*/ |
||||
_setVolumeComponent(component) { |
||||
this._volumeComponent = component; |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { iconName, iconStyle, style, underlayColor } = this.props; |
||||
|
||||
return ( |
||||
<View> |
||||
<ToolbarButton |
||||
iconName = { iconName } |
||||
iconStyle = { iconStyle } |
||||
onClick = { this._onClick } |
||||
style = { style } |
||||
underlayColor = { underlayColor } /> |
||||
{ |
||||
MPVolumeView |
||||
&& <MPVolumeView |
||||
ref = { this._setVolumeComponent } |
||||
style = { HIDE_VIEW_STYLE } /> |
||||
} |
||||
</View> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default connect()(AudioRouteButton); |
Loading…
Reference in new issue