1# Copyright 2021-2024 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16# -----------------------------------------------------------------------------
17# Imports
18# -----------------------------------------------------------------------------
19from __future__ import annotations
20
21import asyncio
22import dataclasses
23import enum
24import struct
25
26from bumble import core
27from bumble import device
28from bumble import gatt
29from bumble import gatt_client
30from bumble import utils
31
32from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
33from typing_extensions import Self
34
35# -----------------------------------------------------------------------------
36# Constants
37# -----------------------------------------------------------------------------
38
39
40class PlayingOrder(utils.OpenIntEnum):
41    '''See Media Control Service 3.15. Playing Order.'''
42
43    SINGLE_ONCE = 0x01
44    SINGLE_REPEAT = 0x02
45    IN_ORDER_ONCE = 0x03
46    IN_ORDER_REPEAT = 0x04
47    OLDEST_ONCE = 0x05
48    OLDEST_REPEAT = 0x06
49    NEWEST_ONCE = 0x07
50    NEWEST_REPEAT = 0x08
51    SHUFFLE_ONCE = 0x09
52    SHUFFLE_REPEAT = 0x0A
53
54
55class PlayingOrderSupported(enum.IntFlag):
56    '''See Media Control Service 3.16. Playing Orders Supported.'''
57
58    SINGLE_ONCE = 0x0001
59    SINGLE_REPEAT = 0x0002
60    IN_ORDER_ONCE = 0x0004
61    IN_ORDER_REPEAT = 0x0008
62    OLDEST_ONCE = 0x0010
63    OLDEST_REPEAT = 0x0020
64    NEWEST_ONCE = 0x0040
65    NEWEST_REPEAT = 0x0080
66    SHUFFLE_ONCE = 0x0100
67    SHUFFLE_REPEAT = 0x0200
68
69
70class MediaState(utils.OpenIntEnum):
71    '''See Media Control Service 3.17. Media State.'''
72
73    INACTIVE = 0x00
74    PLAYING = 0x01
75    PAUSED = 0x02
76    SEEKING = 0x03
77
78
79class MediaControlPointOpcode(utils.OpenIntEnum):
80    '''See Media Control Service 3.18. Media Control Point.'''
81
82    PLAY = 0x01
83    PAUSE = 0x02
84    FAST_REWIND = 0x03
85    FAST_FORWARD = 0x04
86    STOP = 0x05
87    MOVE_RELATIVE = 0x10
88    PREVIOUS_SEGMENT = 0x20
89    NEXT_SEGMENT = 0x21
90    FIRST_SEGMENT = 0x22
91    LAST_SEGMENT = 0x23
92    GOTO_SEGMENT = 0x24
93    PREVIOUS_TRACK = 0x30
94    NEXT_TRACK = 0x31
95    FIRST_TRACK = 0x32
96    LAST_TRACK = 0x33
97    GOTO_TRACK = 0x34
98    PREVIOUS_GROUP = 0x40
99    NEXT_GROUP = 0x41
100    FIRST_GROUP = 0x42
101    LAST_GROUP = 0x43
102    GOTO_GROUP = 0x44
103
104
105class MediaControlPointResultCode(enum.IntFlag):
106    '''See Media Control Service 3.18.2. Media Control Point Notification.'''
107
108    SUCCESS = 0x01
109    OPCODE_NOT_SUPPORTED = 0x02
110    MEDIA_PLAYER_INACTIVE = 0x03
111    COMMAND_CANNOT_BE_COMPLETED = 0x04
112
113
114class MediaControlPointOpcodeSupported(enum.IntFlag):
115    '''See Media Control Service 3.19. Media Control Point Opcodes Supported.'''
116
117    PLAY = 0x00000001
118    PAUSE = 0x00000002
119    FAST_REWIND = 0x00000004
120    FAST_FORWARD = 0x00000008
121    STOP = 0x00000010
122    MOVE_RELATIVE = 0x00000020
123    PREVIOUS_SEGMENT = 0x00000040
124    NEXT_SEGMENT = 0x00000080
125    FIRST_SEGMENT = 0x00000100
126    LAST_SEGMENT = 0x00000200
127    GOTO_SEGMENT = 0x00000400
128    PREVIOUS_TRACK = 0x00000800
129    NEXT_TRACK = 0x00001000
130    FIRST_TRACK = 0x00002000
131    LAST_TRACK = 0x00004000
132    GOTO_TRACK = 0x00008000
133    PREVIOUS_GROUP = 0x00010000
134    NEXT_GROUP = 0x00020000
135    FIRST_GROUP = 0x00040000
136    LAST_GROUP = 0x00080000
137    GOTO_GROUP = 0x00100000
138
139
140class SearchControlPointItemType(utils.OpenIntEnum):
141    '''See Media Control Service 3.20. Search Control Point.'''
142
143    TRACK_NAME = 0x01
144    ARTIST_NAME = 0x02
145    ALBUM_NAME = 0x03
146    GROUP_NAME = 0x04
147    EARLIEST_YEAR = 0x05
148    LATEST_YEAR = 0x06
149    GENRE = 0x07
150    ONLY_TRACKS = 0x08
151    ONLY_GROUPS = 0x09
152
153
154class ObjectType(utils.OpenIntEnum):
155    '''See Media Control Service 4.4.1. Object Type field.'''
156
157    TASK = 0
158    GROUP = 1
159
160
161# -----------------------------------------------------------------------------
162# Classes
163# -----------------------------------------------------------------------------
164
165
166class ObjectId(int):
167    '''See Media Control Service 4.4.2. Object ID field.'''
168
169    @classmethod
170    def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
171        return cls(int.from_bytes(data, byteorder='little', signed=False))
172
173    def __bytes__(self) -> bytes:
174        return self.to_bytes(6, 'little')
175
176
177@dataclasses.dataclass
178class GroupObjectType:
179    '''See Media Control Service 4.4. Group Object Type.'''
180
181    object_type: ObjectType
182    object_id: ObjectId
183
184    @classmethod
185    def from_bytes(cls: Type[Self], data: bytes) -> Self:
186        return cls(
187            object_type=ObjectType(data[0]),
188            object_id=ObjectId.create_from_bytes(data[1:]),
189        )
190
191    def __bytes__(self) -> bytes:
192        return bytes([self.object_type]) + bytes(self.object_id)
193
194
195# -----------------------------------------------------------------------------
196# Server
197# -----------------------------------------------------------------------------
198class MediaControlService(gatt.TemplateService):
199    '''Media Control Service server implementation, only for testing currently.'''
200
201    UUID = gatt.GATT_MEDIA_CONTROL_SERVICE
202
203    def __init__(self, media_player_name: Optional[str] = None) -> None:
204        self.track_position = 0
205
206        self.media_player_name_characteristic = gatt.Characteristic(
207            uuid=gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
208            properties=gatt.Characteristic.Properties.READ
209            | gatt.Characteristic.Properties.NOTIFY,
210            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
211            value=media_player_name or 'Bumble Player',
212        )
213        self.track_changed_characteristic = gatt.Characteristic(
214            uuid=gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
215            properties=gatt.Characteristic.Properties.NOTIFY,
216            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
217            value=b'',
218        )
219        self.track_title_characteristic = gatt.Characteristic(
220            uuid=gatt.GATT_TRACK_TITLE_CHARACTERISTIC,
221            properties=gatt.Characteristic.Properties.READ
222            | gatt.Characteristic.Properties.NOTIFY,
223            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
224            value=b'',
225        )
226        self.track_duration_characteristic = gatt.Characteristic(
227            uuid=gatt.GATT_TRACK_DURATION_CHARACTERISTIC,
228            properties=gatt.Characteristic.Properties.READ
229            | gatt.Characteristic.Properties.NOTIFY,
230            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
231            value=b'',
232        )
233        self.track_position_characteristic = gatt.Characteristic(
234            uuid=gatt.GATT_TRACK_POSITION_CHARACTERISTIC,
235            properties=gatt.Characteristic.Properties.READ
236            | gatt.Characteristic.Properties.WRITE
237            | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
238            | gatt.Characteristic.Properties.NOTIFY,
239            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
240            | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
241            value=b'',
242        )
243        self.media_state_characteristic = gatt.Characteristic(
244            uuid=gatt.GATT_MEDIA_STATE_CHARACTERISTIC,
245            properties=gatt.Characteristic.Properties.READ
246            | gatt.Characteristic.Properties.NOTIFY,
247            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
248            value=b'',
249        )
250        self.media_control_point_characteristic = gatt.Characteristic(
251            uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
252            properties=gatt.Characteristic.Properties.WRITE
253            | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
254            | gatt.Characteristic.Properties.NOTIFY,
255            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
256            | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
257            value=gatt.CharacteristicValue(write=self.on_media_control_point),
258        )
259        self.media_control_point_opcodes_supported_characteristic = gatt.Characteristic(
260            uuid=gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
261            properties=gatt.Characteristic.Properties.READ
262            | gatt.Characteristic.Properties.NOTIFY,
263            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
264            value=b'',
265        )
266        self.content_control_id_characteristic = gatt.Characteristic(
267            uuid=gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
268            properties=gatt.Characteristic.Properties.READ,
269            permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
270            value=b'',
271        )
272
273        super().__init__(
274            [
275                self.media_player_name_characteristic,
276                self.track_changed_characteristic,
277                self.track_title_characteristic,
278                self.track_duration_characteristic,
279                self.track_position_characteristic,
280                self.media_state_characteristic,
281                self.media_control_point_characteristic,
282                self.media_control_point_opcodes_supported_characteristic,
283                self.content_control_id_characteristic,
284            ]
285        )
286
287    async def on_media_control_point(
288        self, connection: Optional[device.Connection], data: bytes
289    ) -> None:
290        if not connection:
291            raise core.InvalidStateError()
292
293        opcode = MediaControlPointOpcode(data[0])
294
295        await connection.device.notify_subscriber(
296            connection,
297            self.media_control_point_characteristic,
298            value=bytes([opcode, MediaControlPointResultCode.SUCCESS]),
299        )
300
301
302class GenericMediaControlService(MediaControlService):
303    UUID = gatt.GATT_GENERIC_MEDIA_CONTROL_SERVICE
304
305
306# -----------------------------------------------------------------------------
307# Client
308# -----------------------------------------------------------------------------
309class MediaControlServiceProxy(
310    gatt_client.ProfileServiceProxy, utils.CompositeEventEmitter
311):
312    SERVICE_CLASS = MediaControlService
313
314    _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
315        'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
316        'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
317        'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
318        'track_changed': gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
319        'track_title': gatt.GATT_TRACK_TITLE_CHARACTERISTIC,
320        'track_duration': gatt.GATT_TRACK_DURATION_CHARACTERISTIC,
321        'track_position': gatt.GATT_TRACK_POSITION_CHARACTERISTIC,
322        'playback_speed': gatt.GATT_PLAYBACK_SPEED_CHARACTERISTIC,
323        'seeking_speed': gatt.GATT_SEEKING_SPEED_CHARACTERISTIC,
324        'current_track_segments_object_id': gatt.GATT_CURRENT_TRACK_SEGMENTS_OBJECT_ID_CHARACTERISTIC,
325        'current_track_object_id': gatt.GATT_CURRENT_TRACK_OBJECT_ID_CHARACTERISTIC,
326        'next_track_object_id': gatt.GATT_NEXT_TRACK_OBJECT_ID_CHARACTERISTIC,
327        'parent_group_object_id': gatt.GATT_PARENT_GROUP_OBJECT_ID_CHARACTERISTIC,
328        'current_group_object_id': gatt.GATT_CURRENT_GROUP_OBJECT_ID_CHARACTERISTIC,
329        'playing_order': gatt.GATT_PLAYING_ORDER_CHARACTERISTIC,
330        'playing_orders_supported': gatt.GATT_PLAYING_ORDERS_SUPPORTED_CHARACTERISTIC,
331        'media_state': gatt.GATT_MEDIA_STATE_CHARACTERISTIC,
332        'media_control_point': gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
333        'media_control_point_opcodes_supported': gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
334        'search_control_point': gatt.GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC,
335        'search_results_object_id': gatt.GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC,
336        'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
337    }
338
339    media_player_name: Optional[gatt_client.CharacteristicProxy] = None
340    media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None
341    media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None
342    track_changed: Optional[gatt_client.CharacteristicProxy] = None
343    track_title: Optional[gatt_client.CharacteristicProxy] = None
344    track_duration: Optional[gatt_client.CharacteristicProxy] = None
345    track_position: Optional[gatt_client.CharacteristicProxy] = None
346    playback_speed: Optional[gatt_client.CharacteristicProxy] = None
347    seeking_speed: Optional[gatt_client.CharacteristicProxy] = None
348    current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None
349    current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
350    next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
351    parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
352    current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
353    playing_order: Optional[gatt_client.CharacteristicProxy] = None
354    playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None
355    media_state: Optional[gatt_client.CharacteristicProxy] = None
356    media_control_point: Optional[gatt_client.CharacteristicProxy] = None
357    media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = (
358        None
359    )
360    search_control_point: Optional[gatt_client.CharacteristicProxy] = None
361    search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None
362    content_control_id: Optional[gatt_client.CharacteristicProxy] = None
363
364    if TYPE_CHECKING:
365        media_control_point_notifications: asyncio.Queue[bytes]
366
367    def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
368        utils.CompositeEventEmitter.__init__(self)
369        self.service_proxy = service_proxy
370        self.lock = asyncio.Lock()
371        self.media_control_point_notifications = asyncio.Queue()
372
373        for field, uuid in self._CHARACTERISTICS.items():
374            if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
375                setattr(self, field, characteristics[0])
376
377    async def subscribe_characteristics(self) -> None:
378        if self.media_control_point:
379            await self.media_control_point.subscribe(self._on_media_control_point)
380        if self.media_state:
381            await self.media_state.subscribe(self._on_media_state)
382        if self.track_changed:
383            await self.track_changed.subscribe(self._on_track_changed)
384        if self.track_title:
385            await self.track_title.subscribe(self._on_track_title)
386        if self.track_duration:
387            await self.track_duration.subscribe(self._on_track_duration)
388        if self.track_position:
389            await self.track_position.subscribe(self._on_track_position)
390
391    async def write_control_point(
392        self, opcode: MediaControlPointOpcode
393    ) -> MediaControlPointResultCode:
394        '''Writes a Media Control Point Opcode to peer and waits for the notification.
395
396        The write operation will be executed when there isn't other pending commands.
397
398        Args:
399            opcode: opcode defined in `MediaControlPointOpcode`.
400
401        Returns:
402            Response code provided in `MediaControlPointResultCode`
403
404        Raises:
405            InvalidOperationError: Server does not have Media Control Point Characteristic.
406            InvalidStateError: Server replies a notification with mismatched opcode.
407        '''
408        if not self.media_control_point:
409            raise core.InvalidOperationError("Peer does not have media control point")
410
411        async with self.lock:
412            await self.media_control_point.write_value(
413                bytes([opcode]),
414                with_response=False,
415            )
416
417            (
418                response_opcode,
419                response_code,
420            ) = await self.media_control_point_notifications.get()
421            if response_opcode != opcode:
422                raise core.InvalidStateError(
423                    f"Expected {opcode} notification, but get {response_opcode}"
424                )
425            return MediaControlPointResultCode(response_code)
426
427    def _on_media_control_point(self, data: bytes) -> None:
428        self.media_control_point_notifications.put_nowait(data)
429
430    def _on_media_state(self, data: bytes) -> None:
431        self.emit('media_state', MediaState(data[0]))
432
433    def _on_track_changed(self, data: bytes) -> None:
434        del data
435        self.emit('track_changed')
436
437    def _on_track_title(self, data: bytes) -> None:
438        self.emit('track_title', data.decode("utf-8"))
439
440    def _on_track_duration(self, data: bytes) -> None:
441        self.emit('track_duration', struct.unpack_from('<i', data)[0])
442
443    def _on_track_position(self, data: bytes) -> None:
444        self.emit('track_position', struct.unpack_from('<i', data)[0])
445
446
447class GenericMediaControlServiceProxy(MediaControlServiceProxy):
448    SERVICE_CLASS = GenericMediaControlService
449