1#!/usr/bin/env python3
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16import inspect
17import time
18from acts import asserts
19from acts.controllers.buds_lib.dev_utils import apollo_sink_events
20from acts.test_utils.bt.bt_constants import bt_default_timeout
21
22
23
24def validate_controller(controller, abstract_device_class):
25    """Ensure controller has all methods in abstract_device_class.
26    Also checks method signatures to ensure parameters are satisfied.
27
28    Args:
29        controller: instance of a device controller.
30        abstract_device_class: class definition of an abstract_device interface.
31    Raises:
32         NotImplementedError: if controller is missing one or more methods.
33    """
34    ctlr_methods = inspect.getmembers(controller, predicate=callable)
35    reqd_methods = inspect.getmembers(
36        abstract_device_class, predicate=inspect.ismethod)
37    expected_func_names = {method[0] for method in reqd_methods}
38    controller_func_names = {method[0] for method in ctlr_methods}
39
40    if not controller_func_names.issuperset(expected_func_names):
41        raise NotImplementedError(
42            'Controller {} is missing the following functions: {}'.format(
43                controller.__class__.__name__,
44                repr(expected_func_names - controller_func_names)))
45
46    for func_name in expected_func_names:
47        controller_func = getattr(controller, func_name)
48        required_func = getattr(abstract_device_class, func_name)
49        required_signature = inspect.signature(required_func)
50        if inspect.signature(controller_func) != required_signature:
51            raise NotImplementedError(
52                'Method {} must have the signature {}{}.'.format(
53                    controller_func.__qualname__, controller_func.__name__,
54                    required_signature))
55
56
57class BluetoothHandsfreeAbstractDevice:
58    """Base class for all Bluetooth handsfree abstract devices.
59
60    Desired controller classes should have a corresponding Bluetooth handsfree
61    abstract device class defined in this module.
62    """
63
64    @property
65    def mac_address(self):
66        raise NotImplementedError
67
68    def accept_call(self):
69        raise NotImplementedError()
70
71    def end_call(self):
72        raise NotImplementedError()
73
74    def enter_pairing_mode(self):
75        raise NotImplementedError()
76
77    def next_track(self):
78        raise NotImplementedError()
79
80    def pause(self):
81        raise NotImplementedError()
82
83    def play(self):
84        raise NotImplementedError()
85
86    def power_off(self):
87        raise NotImplementedError()
88
89    def power_on(self):
90        raise NotImplementedError()
91
92    def previous_track(self):
93        raise NotImplementedError()
94
95    def reject_call(self):
96        raise NotImplementedError()
97
98    def volume_down(self):
99        raise NotImplementedError()
100
101    def volume_up(self):
102        raise NotImplementedError()
103
104
105class PixelBudsBluetoothHandsfreeAbstractDevice(
106        BluetoothHandsfreeAbstractDevice):
107
108    CMD_EVENT = 'EvtHex'
109
110    def __init__(self, pixel_buds_controller):
111        self.pixel_buds_controller = pixel_buds_controller
112
113    def format_cmd(self, cmd_name):
114        return self.CMD_EVENT + ' ' + apollo_sink_events.SINK_EVENTS[cmd_name]
115
116    @property
117    def mac_address(self):
118        return self.pixel_buds_controller.bluetooth_address
119
120    def accept_call(self):
121        return self.pixel_buds_controller.cmd(
122            self.format_cmd('EventUsrAnswer'))
123
124    def end_call(self):
125        return self.pixel_buds_controller.cmd(
126            self.format_cmd('EventUsrCancelEnd'))
127
128    def enter_pairing_mode(self):
129        return self.pixel_buds_controller.set_pairing_mode()
130
131    def next_track(self):
132        return self.pixel_buds_controller.cmd(
133            self.format_cmd('EventUsrAvrcpSkipForward'))
134
135    def pause(self):
136        return self.pixel_buds_controller.cmd(
137            self.format_cmd('EventUsrAvrcpPause'))
138
139    def play(self):
140        return self.pixel_buds_controller.cmd(
141            self.format_cmd('EventUsrAvrcpPlay'))
142
143    def power_off(self):
144        return self.pixel_buds_controller.power('Off')
145
146    def power_on(self):
147        return self.pixel_buds_controller.power('On')
148
149    def previous_track(self):
150        return self.pixel_buds_controller.cmd(
151            self.format_cmd('EventUsrAvrcpSkipBackward'))
152
153    def reject_call(self):
154        return self.pixel_buds_controller.cmd(
155            self.format_cmd('EventUsrReject'))
156
157    def volume_down(self):
158        return self.pixel_buds_controller.volume('Down')
159
160    def volume_up(self):
161        return self.pixel_buds_controller.volume('Up')
162
163
164class EarstudioReceiverBluetoothHandsfreeAbstractDevice(
165        BluetoothHandsfreeAbstractDevice):
166    def __init__(self, earstudio_controller):
167        self.earstudio_controller = earstudio_controller
168
169    @property
170    def mac_address(self):
171        return self.earstudio_controller.mac_address
172
173    def accept_call(self):
174        return self.earstudio_controller.press_accept_call()
175
176    def end_call(self):
177        return self.earstudio_controller.press_end_call()
178
179    def enter_pairing_mode(self):
180        return self.earstudio_controller.enter_pairing_mode()
181
182    def next_track(self):
183        return self.earstudio_controller.press_next()
184
185    def pause(self):
186        return self.earstudio_controller.press_play_pause()
187
188    def play(self):
189        return self.earstudio_controller.press_play_pause()
190
191    def power_off(self):
192        return self.earstudio_controller.power_off()
193
194    def power_on(self):
195        return self.earstudio_controller.power_on()
196
197    def previous_track(self):
198        return self.earstudio_controller.press_previous()
199
200    def reject_call(self):
201        return self.earstudio_controller.press_reject_call()
202
203    def volume_down(self):
204        return self.earstudio_controller.press_volume_down()
205
206    def volume_up(self):
207        return self.earstudio_controller.press_volume_up()
208
209
210class JaybirdX3EarbudsBluetoothHandsfreeAbstractDevice(
211        BluetoothHandsfreeAbstractDevice):
212    def __init__(self, jaybird_controller):
213        self.jaybird_controller = jaybird_controller
214
215    @property
216    def mac_address(self):
217        return self.jaybird_controller.mac_address
218
219    def accept_call(self):
220        return self.jaybird_controller.press_accept_call()
221
222    def end_call(self):
223        return self.jaybird_controller.press_reject_call()
224
225    def enter_pairing_mode(self):
226        return self.jaybird_controller.enter_pairing_mode()
227
228    def next_track(self):
229        return self.jaybird_controller.press_next()
230
231    def pause(self):
232        return self.jaybird_controller.press_play_pause()
233
234    def play(self):
235        return self.jaybird_controller.press_play_pause()
236
237    def power_off(self):
238        return self.jaybird_controller.power_off()
239
240    def power_on(self):
241        return self.jaybird_controller.power_on()
242
243    def previous_track(self):
244        return self.jaybird_controller.press_previous()
245
246    def reject_call(self):
247        return self.jaybird_controller.press_reject_call()
248
249    def volume_down(self):
250        return self.jaybird_controller.press_volume_down()
251
252    def volume_up(self):
253        return self.jaybird_controller.press_volume_up()
254
255
256class AndroidHeadsetBluetoothHandsfreeAbstractDevice(
257        BluetoothHandsfreeAbstractDevice):
258    def __init__(self, ad_controller):
259        self.ad_controller = ad_controller
260
261    @property
262    def mac_address(self):
263        """Getting device mac with more stability ensurance.
264
265        Sometime, getting mac address is flaky that it returns None. Adding a
266        loop to add more ensurance of getting correct mac address.
267        """
268        device_mac = None
269        start_time = time.time()
270        end_time = start_time + bt_default_timeout
271        while not device_mac and time.time() < end_time:
272            device_mac = self.ad_controller.droid.bluetoothGetLocalAddress()
273        asserts.assert_true(device_mac, 'Can not get the MAC address')
274        return device_mac
275
276    def accept_call(self):
277        return self.ad_controller.droid.telecomAcceptRingingCall(None)
278
279    def end_call(self):
280        return self.ad_controller.droid.telecomEndCall()
281
282    def enter_pairing_mode(self):
283        self.ad_controller.droid.bluetoothStartPairingHelper(True)
284        return self.ad_controller.droid.bluetoothMakeDiscoverable()
285
286    def next_track(self):
287        return (self.ad_controller.droid.bluetoothMediaPassthrough("skipNext"))
288
289    def pause(self):
290        return self.ad_controller.droid.bluetoothMediaPassthrough("pause")
291
292    def play(self):
293        return self.ad_controller.droid.bluetoothMediaPassthrough("play")
294
295    def power_off(self):
296        return self.ad_controller.droid.bluetoothToggleState(False)
297
298    def power_on(self):
299        return self.ad_controller.droid.bluetoothToggleState(True)
300
301    def previous_track(self):
302        return (self.ad_controller.droid.bluetoothMediaPassthrough("skipPrev"))
303
304    def reject_call(self):
305        return self.ad_controller.droid.telecomCallDisconnect(
306            self.ad_controller.droid.telecomCallGetCallIds()[0])
307
308    def reset(self):
309        return self.ad_controller.droid.bluetoothFactoryReset()
310
311    def volume_down(self):
312        target_step = self.ad_controller.droid.getMediaVolume() - 1
313        target_step = max(target_step, 0)
314        return self.ad_controller.droid.setMediaVolume(target_step)
315
316    def volume_up(self):
317        target_step = self.ad_controller.droid.getMediaVolume() + 1
318        max_step = self.ad_controller.droid.getMaxMediaVolume()
319        target_step = min(target_step, max_step)
320        return self.ad_controller.droid.setMediaVolume(target_step)
321
322
323class BluetoothHandsfreeAbstractDeviceFactory:
324    """Generates a BluetoothHandsfreeAbstractDevice for any device controller.
325    """
326
327    _controller_abstract_devices = {
328        'EarstudioReceiver': EarstudioReceiverBluetoothHandsfreeAbstractDevice,
329        'JaybirdX3Earbuds': JaybirdX3EarbudsBluetoothHandsfreeAbstractDevice,
330        'ParentDevice': PixelBudsBluetoothHandsfreeAbstractDevice,
331        'AndroidDevice': AndroidHeadsetBluetoothHandsfreeAbstractDevice
332    }
333
334    def generate(self, controller):
335        class_name = controller.__class__.__name__
336        if class_name in self._controller_abstract_devices:
337            return self._controller_abstract_devices[class_name](controller)
338        else:
339            validate_controller(controller, BluetoothHandsfreeAbstractDevice)
340            return controller
341