|
1 /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 #include "base/basictypes.h" |
|
8 |
|
9 #include "BluetoothA2dpManager.h" |
|
10 |
|
11 #include <hardware/bluetooth.h> |
|
12 #include <hardware/bt_av.h> |
|
13 #if ANDROID_VERSION > 17 |
|
14 #include <hardware/bt_rc.h> |
|
15 #endif |
|
16 |
|
17 #include "BluetoothCommon.h" |
|
18 #include "BluetoothService.h" |
|
19 #include "BluetoothSocket.h" |
|
20 #include "BluetoothUtils.h" |
|
21 |
|
22 #include "mozilla/dom/bluetooth/BluetoothTypes.h" |
|
23 #include "mozilla/Services.h" |
|
24 #include "mozilla/StaticPtr.h" |
|
25 #include "MainThreadUtils.h" |
|
26 #include "nsIObserverService.h" |
|
27 #include "nsThreadUtils.h" |
|
28 |
|
29 using namespace mozilla; |
|
30 USING_BLUETOOTH_NAMESPACE |
|
31 // AVRC_ID op code follows bluedroid avrc_defs.h |
|
32 #define AVRC_ID_REWIND 0x48 |
|
33 #define AVRC_ID_FAST_FOR 0x49 |
|
34 #define AVRC_KEY_PRESS_STATE 1 |
|
35 #define AVRC_KEY_RELEASE_STATE 0 |
|
36 |
|
37 namespace { |
|
38 StaticRefPtr<BluetoothA2dpManager> sBluetoothA2dpManager; |
|
39 bool sInShutdown = false; |
|
40 static const btav_interface_t* sBtA2dpInterface; |
|
41 #if ANDROID_VERSION > 17 |
|
42 static const btrc_interface_t* sBtAvrcpInterface; |
|
43 #endif |
|
44 } // anonymous namespace |
|
45 |
|
46 class SinkPropertyChangedHandler : public nsRunnable |
|
47 { |
|
48 public: |
|
49 SinkPropertyChangedHandler(const BluetoothSignal& aSignal) |
|
50 : mSignal(aSignal) |
|
51 { |
|
52 } |
|
53 |
|
54 NS_IMETHOD |
|
55 Run() |
|
56 { |
|
57 MOZ_ASSERT(NS_IsMainThread()); |
|
58 |
|
59 BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get(); |
|
60 NS_ENSURE_TRUE(a2dp, NS_ERROR_FAILURE); |
|
61 a2dp->HandleSinkPropertyChanged(mSignal); |
|
62 |
|
63 return NS_OK; |
|
64 } |
|
65 |
|
66 private: |
|
67 BluetoothSignal mSignal; |
|
68 }; |
|
69 |
|
70 class RequestPlayStatusTask : public nsRunnable |
|
71 { |
|
72 public: |
|
73 RequestPlayStatusTask() |
|
74 { |
|
75 MOZ_ASSERT(!NS_IsMainThread()); |
|
76 } |
|
77 |
|
78 nsresult Run() |
|
79 { |
|
80 MOZ_ASSERT(NS_IsMainThread()); |
|
81 |
|
82 BluetoothSignal signal(NS_LITERAL_STRING(REQUEST_MEDIA_PLAYSTATUS_ID), |
|
83 NS_LITERAL_STRING(KEY_ADAPTER), |
|
84 InfallibleTArray<BluetoothNamedValue>()); |
|
85 |
|
86 BluetoothService* bs = BluetoothService::Get(); |
|
87 NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE); |
|
88 bs->DistributeSignal(signal); |
|
89 |
|
90 return NS_OK; |
|
91 } |
|
92 }; |
|
93 |
|
94 #if ANDROID_VERSION > 17 |
|
95 class UpdateRegisterNotificationTask : public nsRunnable |
|
96 { |
|
97 public: |
|
98 UpdateRegisterNotificationTask(btrc_event_id_t aEventId, uint32_t aParam) |
|
99 : mEventId(aEventId) |
|
100 , mParam(aParam) |
|
101 { |
|
102 MOZ_ASSERT(!NS_IsMainThread()); |
|
103 } |
|
104 |
|
105 nsresult Run() |
|
106 { |
|
107 MOZ_ASSERT(NS_IsMainThread()); |
|
108 |
|
109 BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get(); |
|
110 NS_ENSURE_TRUE(a2dp, NS_OK); |
|
111 a2dp->UpdateRegisterNotification(mEventId, mParam); |
|
112 return NS_OK; |
|
113 } |
|
114 private: |
|
115 btrc_event_id_t mEventId; |
|
116 uint32_t mParam; |
|
117 }; |
|
118 |
|
119 /* |
|
120 * This function maps attribute id and returns corresponding values |
|
121 * Attribute id refers to btrc_media_attr_t in bt_rc.h |
|
122 */ |
|
123 static void |
|
124 ConvertAttributeString(int aAttrId, nsAString& aAttrStr) |
|
125 { |
|
126 BluetoothA2dpManager* a2dp = BluetoothA2dpManager::Get(); |
|
127 NS_ENSURE_TRUE_VOID(a2dp); |
|
128 |
|
129 switch (aAttrId) { |
|
130 case BTRC_MEDIA_ATTR_TITLE: |
|
131 a2dp->GetTitle(aAttrStr); |
|
132 break; |
|
133 case BTRC_MEDIA_ATTR_ARTIST: |
|
134 a2dp->GetArtist(aAttrStr); |
|
135 break; |
|
136 case BTRC_MEDIA_ATTR_ALBUM: |
|
137 a2dp->GetAlbum(aAttrStr); |
|
138 break; |
|
139 case BTRC_MEDIA_ATTR_TRACK_NUM: |
|
140 aAttrStr.AppendInt(a2dp->GetMediaNumber()); |
|
141 break; |
|
142 case BTRC_MEDIA_ATTR_NUM_TRACKS: |
|
143 aAttrStr.AppendInt(a2dp->GetTotalMediaNumber()); |
|
144 break; |
|
145 case BTRC_MEDIA_ATTR_GENRE: |
|
146 // TODO: we currently don't support genre from music player |
|
147 aAttrStr.Truncate(); |
|
148 break; |
|
149 case BTRC_MEDIA_ATTR_PLAYING_TIME: |
|
150 aAttrStr.AppendInt(a2dp->GetDuration()); |
|
151 break; |
|
152 } |
|
153 } |
|
154 |
|
155 class UpdateElementAttrsTask : public nsRunnable |
|
156 { |
|
157 public: |
|
158 UpdateElementAttrsTask(uint8_t aNumAttr, btrc_media_attr_t* aPlayerAttrs) |
|
159 : mNumAttr(aNumAttr) |
|
160 , mPlayerAttrs(aPlayerAttrs) |
|
161 { |
|
162 MOZ_ASSERT(!NS_IsMainThread()); |
|
163 } |
|
164 |
|
165 nsresult Run() |
|
166 { |
|
167 MOZ_ASSERT(NS_IsMainThread()); |
|
168 |
|
169 btrc_element_attr_val_t* attrs = new btrc_element_attr_val_t[mNumAttr]; |
|
170 for (int i = 0; i < mNumAttr; i++) { |
|
171 nsAutoString attrText; |
|
172 attrs[i].attr_id = mPlayerAttrs[i]; |
|
173 ConvertAttributeString(mPlayerAttrs[i], attrText); |
|
174 strcpy((char *)attrs[i].text, NS_ConvertUTF16toUTF8(attrText).get()); |
|
175 } |
|
176 |
|
177 NS_ENSURE_TRUE(sBtAvrcpInterface, NS_OK); |
|
178 sBtAvrcpInterface->get_element_attr_rsp(mNumAttr, attrs); |
|
179 |
|
180 return NS_OK; |
|
181 } |
|
182 private: |
|
183 uint8_t mNumAttr; |
|
184 btrc_media_attr_t* mPlayerAttrs; |
|
185 }; |
|
186 |
|
187 class UpdatePassthroughCmdTask : public nsRunnable |
|
188 { |
|
189 public: |
|
190 UpdatePassthroughCmdTask(const nsAString& aName) |
|
191 : mName(aName) |
|
192 { |
|
193 MOZ_ASSERT(!NS_IsMainThread()); |
|
194 } |
|
195 |
|
196 nsresult Run() |
|
197 { |
|
198 MOZ_ASSERT(NS_IsMainThread()); |
|
199 |
|
200 NS_NAMED_LITERAL_STRING(type, "media-button"); |
|
201 BroadcastSystemMessage(type, BluetoothValue(mName)); |
|
202 |
|
203 return NS_OK; |
|
204 } |
|
205 private: |
|
206 nsString mName; |
|
207 }; |
|
208 |
|
209 #endif |
|
210 |
|
211 NS_IMETHODIMP |
|
212 BluetoothA2dpManager::Observe(nsISupports* aSubject, |
|
213 const char* aTopic, |
|
214 const char16_t* aData) |
|
215 { |
|
216 MOZ_ASSERT(sBluetoothA2dpManager); |
|
217 |
|
218 if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { |
|
219 HandleShutdown(); |
|
220 return NS_OK; |
|
221 } |
|
222 |
|
223 MOZ_ASSERT(false, "BluetoothA2dpManager got unexpected topic!"); |
|
224 return NS_ERROR_UNEXPECTED; |
|
225 } |
|
226 |
|
227 BluetoothA2dpManager::BluetoothA2dpManager() |
|
228 { |
|
229 Reset(); |
|
230 } |
|
231 |
|
232 void |
|
233 BluetoothA2dpManager::Reset() |
|
234 { |
|
235 ResetA2dp(); |
|
236 ResetAvrcp(); |
|
237 } |
|
238 |
|
239 static void |
|
240 AvStatusToSinkString(btav_connection_state_t aStatus, nsAString& aState) |
|
241 { |
|
242 nsAutoString state; |
|
243 if (aStatus == BTAV_CONNECTION_STATE_DISCONNECTED) { |
|
244 aState = NS_LITERAL_STRING("disconnected"); |
|
245 } else if (aStatus == BTAV_CONNECTION_STATE_CONNECTING) { |
|
246 aState = NS_LITERAL_STRING("connecting"); |
|
247 } else if (aStatus == BTAV_CONNECTION_STATE_CONNECTED) { |
|
248 aState = NS_LITERAL_STRING("connected"); |
|
249 } else if (aStatus == BTAV_CONNECTION_STATE_DISCONNECTING) { |
|
250 aState = NS_LITERAL_STRING("disconnecting"); |
|
251 } else { |
|
252 BT_WARNING("Unknown sink state"); |
|
253 } |
|
254 } |
|
255 |
|
256 static void |
|
257 A2dpConnectionStateCallback(btav_connection_state_t aState, |
|
258 bt_bdaddr_t* aBdAddress) |
|
259 { |
|
260 MOZ_ASSERT(!NS_IsMainThread()); |
|
261 |
|
262 nsString remoteDeviceBdAddress; |
|
263 BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress); |
|
264 |
|
265 nsString a2dpState; |
|
266 AvStatusToSinkString(aState, a2dpState); |
|
267 |
|
268 InfallibleTArray<BluetoothNamedValue> props; |
|
269 BT_APPEND_NAMED_VALUE(props, "State", a2dpState); |
|
270 |
|
271 BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"), |
|
272 remoteDeviceBdAddress, props); |
|
273 NS_DispatchToMainThread(new SinkPropertyChangedHandler(signal)); |
|
274 } |
|
275 |
|
276 static void |
|
277 A2dpAudioStateCallback(btav_audio_state_t aState, |
|
278 bt_bdaddr_t* aBdAddress) |
|
279 { |
|
280 MOZ_ASSERT(!NS_IsMainThread()); |
|
281 |
|
282 nsString remoteDeviceBdAddress; |
|
283 BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress); |
|
284 |
|
285 nsString a2dpState; |
|
286 |
|
287 if (aState == BTAV_AUDIO_STATE_STARTED) { |
|
288 a2dpState = NS_LITERAL_STRING("playing"); |
|
289 } else if (aState == BTAV_AUDIO_STATE_STOPPED) { |
|
290 // for avdtp state stop stream |
|
291 a2dpState = NS_LITERAL_STRING("connected"); |
|
292 } else if (aState == BTAV_AUDIO_STATE_REMOTE_SUSPEND) { |
|
293 // for avdtp state suspend stream from remote side |
|
294 a2dpState = NS_LITERAL_STRING("connected"); |
|
295 } |
|
296 |
|
297 InfallibleTArray<BluetoothNamedValue> props; |
|
298 BT_APPEND_NAMED_VALUE(props, "State", a2dpState); |
|
299 |
|
300 BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"), |
|
301 remoteDeviceBdAddress, props); |
|
302 NS_DispatchToMainThread(new SinkPropertyChangedHandler(signal)); |
|
303 } |
|
304 |
|
305 #if ANDROID_VERSION > 17 |
|
306 /* |
|
307 * Avrcp 1.3 callbacks |
|
308 */ |
|
309 |
|
310 /* |
|
311 * This function is to request Gaia player application to update |
|
312 * current play status. |
|
313 * Callback for play status request |
|
314 */ |
|
315 static void |
|
316 AvrcpGetPlayStatusCallback() |
|
317 { |
|
318 MOZ_ASSERT(!NS_IsMainThread()); |
|
319 |
|
320 NS_DispatchToMainThread(new RequestPlayStatusTask()); |
|
321 } |
|
322 |
|
323 /* |
|
324 * This function is trying to get element attributes, which request from CT |
|
325 * Unlike BlueZ only calls UpdateMetaData, bluedroid does not cache meta data |
|
326 * information, but instead uses callback AvrcpGetElementAttrCallback and |
|
327 * call get_element_attr_rsp() to reply request. |
|
328 * |
|
329 * Callback to fetch the get element attributes of the current song |
|
330 * aNumAttr: It represents the number of attributes requested in aPlayerAttrs |
|
331 * aPlayerAttrs: It represents Attribute Ids |
|
332 */ |
|
333 static void |
|
334 AvrcpGetElementAttrCallback(uint8_t aNumAttr, btrc_media_attr_t* aPlayerAttrs) |
|
335 { |
|
336 MOZ_ASSERT(!NS_IsMainThread()); |
|
337 |
|
338 NS_DispatchToMainThread(new UpdateElementAttrsTask(aNumAttr, aPlayerAttrs)); |
|
339 } |
|
340 |
|
341 /* |
|
342 * Callback for register notification (Play state change/track change/...) |
|
343 * To reply RegisterNotification INTERIM response |
|
344 * See AVRCP 1.3 Spec 25.2 |
|
345 * aParam: It only valids if event_id is BTRC_EVT_PLAY_POS_CHANGED, |
|
346 * which is playback interval time |
|
347 */ |
|
348 static void |
|
349 AvrcpRegisterNotificationCallback(btrc_event_id_t aEventId, uint32_t aParam) |
|
350 { |
|
351 MOZ_ASSERT(!NS_IsMainThread()); |
|
352 |
|
353 NS_DispatchToMainThread(new UpdateRegisterNotificationTask(aEventId, aParam)); |
|
354 } |
|
355 |
|
356 /* |
|
357 * Player application settings is optional for Avrcp 1.3 |
|
358 * B2G 1.3 currently does not support Player application setting |
|
359 * related functions. Support Player Setting in the future version |
|
360 */ |
|
361 static void |
|
362 AvrcpListPlayerAppAttributeCallback() |
|
363 { |
|
364 MOZ_ASSERT(!NS_IsMainThread()); |
|
365 |
|
366 // TODO: Support avrcp application setting related functions |
|
367 } |
|
368 |
|
369 static void |
|
370 AvrcpListPlayerAppValuesCallback(btrc_player_attr_t aAttrId) |
|
371 { |
|
372 MOZ_ASSERT(!NS_IsMainThread()); |
|
373 |
|
374 // TODO: Support avrcp application setting related functions |
|
375 } |
|
376 |
|
377 static void |
|
378 AvrcpGetPlayerAppValueCallback(uint8_t aNumAttr, |
|
379 btrc_player_attr_t* aPlayerAttrs) |
|
380 { |
|
381 MOZ_ASSERT(!NS_IsMainThread()); |
|
382 |
|
383 // TODO: Support avrcp application setting related functions |
|
384 } |
|
385 |
|
386 static void |
|
387 AvrcpGetPlayerAppAttrsTextCallback(uint8_t aNumAttr, |
|
388 btrc_player_attr_t* PlayerAttrs) |
|
389 { |
|
390 MOZ_ASSERT(!NS_IsMainThread()); |
|
391 |
|
392 // TODO: Support avrcp application setting related functions |
|
393 } |
|
394 |
|
395 static void |
|
396 AvrcpGetPlayerAppValuesTextCallback(uint8_t aAttrId, uint8_t aNumVal, |
|
397 uint8_t* PlayerVals) |
|
398 { |
|
399 MOZ_ASSERT(!NS_IsMainThread()); |
|
400 |
|
401 // TODO: Support avrcp application setting related functions |
|
402 } |
|
403 |
|
404 static void |
|
405 AvrcpSetPlayerAppValueCallback(btrc_player_settings_t* aPlayerVals) |
|
406 { |
|
407 MOZ_ASSERT(!NS_IsMainThread()); |
|
408 |
|
409 // TODO: Support avrcp application setting related functions |
|
410 } |
|
411 #endif |
|
412 |
|
413 #if ANDROID_VERSION > 18 |
|
414 /* |
|
415 * This callback function is to get CT features from Feature Bit Mask. |
|
416 * If Advanced Control Player bit is set, CT supports |
|
417 * volume sync (absolute volume feature). If Browsing bit is set, Avrcp 1.4 |
|
418 * Browse feature will be supported |
|
419 */ |
|
420 static void |
|
421 AvrcpRemoteFeaturesCallback(bt_bdaddr_t* aBdAddress, |
|
422 btrc_remote_features_t aFeatures) |
|
423 { |
|
424 // TODO: Support avrcp 1.4 absolute volume/browse |
|
425 } |
|
426 |
|
427 /* |
|
428 * This callback function is to get notification that volume changed on the |
|
429 * remote car kit (if it supports Avrcp 1.4), not notification from phone. |
|
430 */ |
|
431 static void |
|
432 AvrcpRemoteVolumeChangedCallback(uint8_t aVolume, uint8_t aCType) |
|
433 { |
|
434 // TODO: Support avrcp 1.4 absolute volume/browse |
|
435 } |
|
436 |
|
437 /* |
|
438 * This callback function is to handle passthrough commands. |
|
439 */ |
|
440 static void |
|
441 AvrcpPassThroughCallback(int aId, int aKeyState) |
|
442 { |
|
443 // Fast-forward and rewind key events won't be generated from bluedroid |
|
444 // stack after ANDROID_VERSION > 18, but via passthrough callback. |
|
445 nsAutoString name; |
|
446 NS_ENSURE_TRUE_VOID(aKeyState == AVRC_KEY_PRESS_STATE || |
|
447 aKeyState == AVRC_KEY_RELEASE_STATE); |
|
448 switch (aId) { |
|
449 case AVRC_ID_FAST_FOR: |
|
450 if (aKeyState == AVRC_KEY_PRESS_STATE) { |
|
451 name.AssignLiteral("media-fast-forward-button-press"); |
|
452 } else { |
|
453 name.AssignLiteral("media-fast-forward-button-release"); |
|
454 } |
|
455 break; |
|
456 case AVRC_ID_REWIND: |
|
457 if (aKeyState == AVRC_KEY_PRESS_STATE) { |
|
458 name.AssignLiteral("media-rewind-button-press"); |
|
459 } else { |
|
460 name.AssignLiteral("media-rewind-button-release"); |
|
461 } |
|
462 break; |
|
463 default: |
|
464 BT_WARNING("Unable to handle the unknown PassThrough command %d", aId); |
|
465 break; |
|
466 } |
|
467 if (!name.IsEmpty()) { |
|
468 NS_DispatchToMainThread(new UpdatePassthroughCmdTask(name)); |
|
469 } |
|
470 } |
|
471 #endif |
|
472 |
|
473 static btav_callbacks_t sBtA2dpCallbacks = { |
|
474 sizeof(sBtA2dpCallbacks), |
|
475 A2dpConnectionStateCallback, |
|
476 A2dpAudioStateCallback |
|
477 }; |
|
478 |
|
479 #if ANDROID_VERSION > 17 |
|
480 static btrc_callbacks_t sBtAvrcpCallbacks = { |
|
481 sizeof(sBtAvrcpCallbacks), |
|
482 #if ANDROID_VERSION > 18 |
|
483 AvrcpRemoteFeaturesCallback, |
|
484 #endif |
|
485 AvrcpGetPlayStatusCallback, |
|
486 AvrcpListPlayerAppAttributeCallback, |
|
487 AvrcpListPlayerAppValuesCallback, |
|
488 AvrcpGetPlayerAppValueCallback, |
|
489 AvrcpGetPlayerAppAttrsTextCallback, |
|
490 AvrcpGetPlayerAppValuesTextCallback, |
|
491 AvrcpSetPlayerAppValueCallback, |
|
492 AvrcpGetElementAttrCallback, |
|
493 AvrcpRegisterNotificationCallback, |
|
494 #if ANDROID_VERSION > 18 |
|
495 AvrcpRemoteVolumeChangedCallback, |
|
496 AvrcpPassThroughCallback |
|
497 #endif |
|
498 }; |
|
499 #endif |
|
500 |
|
501 /* |
|
502 * This function will be only called when Bluetooth is turning on. |
|
503 * It is important to register a2dp callbacks before enable() gets called. |
|
504 * It is required to register a2dp callbacks before a2dp media task |
|
505 * starts up. |
|
506 */ |
|
507 bool |
|
508 BluetoothA2dpManager::Init() |
|
509 { |
|
510 const bt_interface_t* btInf = GetBluetoothInterface(); |
|
511 NS_ENSURE_TRUE(btInf, false); |
|
512 |
|
513 sBtA2dpInterface = (btav_interface_t *)btInf-> |
|
514 get_profile_interface(BT_PROFILE_ADVANCED_AUDIO_ID); |
|
515 NS_ENSURE_TRUE(sBtA2dpInterface, false); |
|
516 |
|
517 int ret = sBtA2dpInterface->init(&sBtA2dpCallbacks); |
|
518 if (ret != BT_STATUS_SUCCESS) { |
|
519 BT_LOGR("Warning: failed to init a2dp module"); |
|
520 return false; |
|
521 } |
|
522 |
|
523 #if ANDROID_VERSION > 17 |
|
524 sBtAvrcpInterface = (btrc_interface_t *)btInf-> |
|
525 get_profile_interface(BT_PROFILE_AV_RC_ID); |
|
526 NS_ENSURE_TRUE(sBtAvrcpInterface, false); |
|
527 |
|
528 ret = sBtAvrcpInterface->init(&sBtAvrcpCallbacks); |
|
529 if (ret != BT_STATUS_SUCCESS) { |
|
530 BT_LOGR("Warning: failed to init avrcp module"); |
|
531 return false; |
|
532 } |
|
533 #endif |
|
534 |
|
535 return true; |
|
536 } |
|
537 |
|
538 BluetoothA2dpManager::~BluetoothA2dpManager() |
|
539 { |
|
540 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); |
|
541 NS_ENSURE_TRUE_VOID(obs); |
|
542 if (NS_FAILED(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID))) { |
|
543 BT_WARNING("Failed to remove shutdown observer!"); |
|
544 } |
|
545 } |
|
546 |
|
547 void |
|
548 BluetoothA2dpManager::ResetA2dp() |
|
549 { |
|
550 mA2dpConnected = false; |
|
551 mSinkState = SinkState::SINK_DISCONNECTED; |
|
552 mController = nullptr; |
|
553 } |
|
554 |
|
555 void |
|
556 BluetoothA2dpManager::ResetAvrcp() |
|
557 { |
|
558 mAvrcpConnected = false; |
|
559 mDuration = 0; |
|
560 mMediaNumber = 0; |
|
561 mTotalMediaCount = 0; |
|
562 mPosition = 0; |
|
563 mPlayStatus = ControlPlayStatus::PLAYSTATUS_UNKNOWN; |
|
564 } |
|
565 |
|
566 /* |
|
567 * Static functions |
|
568 */ |
|
569 |
|
570 static BluetoothA2dpManager::SinkState |
|
571 StatusStringToSinkState(const nsAString& aStatus) |
|
572 { |
|
573 BluetoothA2dpManager::SinkState state = |
|
574 BluetoothA2dpManager::SinkState::SINK_UNKNOWN; |
|
575 if (aStatus.EqualsLiteral("disconnected")) { |
|
576 state = BluetoothA2dpManager::SinkState::SINK_DISCONNECTED; |
|
577 } else if (aStatus.EqualsLiteral("connecting")) { |
|
578 state = BluetoothA2dpManager::SinkState::SINK_CONNECTING; |
|
579 } else if (aStatus.EqualsLiteral("connected")) { |
|
580 state = BluetoothA2dpManager::SinkState::SINK_CONNECTED; |
|
581 } else if (aStatus.EqualsLiteral("playing")) { |
|
582 state = BluetoothA2dpManager::SinkState::SINK_PLAYING; |
|
583 } else { |
|
584 BT_WARNING("Unknown sink state"); |
|
585 } |
|
586 return state; |
|
587 } |
|
588 |
|
589 //static |
|
590 BluetoothA2dpManager* |
|
591 BluetoothA2dpManager::Get() |
|
592 { |
|
593 MOZ_ASSERT(NS_IsMainThread()); |
|
594 |
|
595 // If sBluetoothA2dpManager already exists, exit early |
|
596 if (sBluetoothA2dpManager) { |
|
597 return sBluetoothA2dpManager; |
|
598 } |
|
599 |
|
600 // If we're in shutdown, don't create a new instance |
|
601 NS_ENSURE_FALSE(sInShutdown, nullptr); |
|
602 |
|
603 // Create a new instance, register, and return |
|
604 BluetoothA2dpManager* manager = new BluetoothA2dpManager(); |
|
605 NS_ENSURE_TRUE(manager->Init(), nullptr); |
|
606 |
|
607 sBluetoothA2dpManager = manager; |
|
608 return sBluetoothA2dpManager; |
|
609 } |
|
610 |
|
611 void |
|
612 BluetoothA2dpManager::HandleShutdown() |
|
613 { |
|
614 MOZ_ASSERT(NS_IsMainThread()); |
|
615 sInShutdown = true; |
|
616 Disconnect(nullptr); |
|
617 sBluetoothA2dpManager = nullptr; |
|
618 } |
|
619 |
|
620 void |
|
621 BluetoothA2dpManager::Connect(const nsAString& aDeviceAddress, |
|
622 BluetoothProfileController* aController) |
|
623 { |
|
624 MOZ_ASSERT(NS_IsMainThread()); |
|
625 MOZ_ASSERT(!aDeviceAddress.IsEmpty()); |
|
626 MOZ_ASSERT(aController && !mController); |
|
627 |
|
628 BluetoothService* bs = BluetoothService::Get(); |
|
629 if (!bs || sInShutdown) { |
|
630 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); |
|
631 return; |
|
632 } |
|
633 |
|
634 if (mA2dpConnected) { |
|
635 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_ALREADY_CONNECTED)); |
|
636 return; |
|
637 } |
|
638 |
|
639 mDeviceAddress = aDeviceAddress; |
|
640 mController = aController; |
|
641 |
|
642 if (!sBtA2dpInterface) { |
|
643 BT_LOGR("sBluetoothA2dpInterface is null"); |
|
644 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); |
|
645 return; |
|
646 } |
|
647 |
|
648 bt_bdaddr_t remoteAddress; |
|
649 StringToBdAddressType(aDeviceAddress, &remoteAddress); |
|
650 |
|
651 bt_status_t result = sBtA2dpInterface->connect(&remoteAddress); |
|
652 if (BT_STATUS_SUCCESS != result) { |
|
653 BT_LOGR("Failed to connect: %x", result); |
|
654 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_CONNECTION_FAILED)); |
|
655 return; |
|
656 } |
|
657 } |
|
658 |
|
659 void |
|
660 BluetoothA2dpManager::Disconnect(BluetoothProfileController* aController) |
|
661 { |
|
662 MOZ_ASSERT(NS_IsMainThread()); |
|
663 MOZ_ASSERT(!mController); |
|
664 |
|
665 BluetoothService* bs = BluetoothService::Get(); |
|
666 if (!bs) { |
|
667 if (aController) { |
|
668 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); |
|
669 } |
|
670 return; |
|
671 } |
|
672 |
|
673 if (!mA2dpConnected) { |
|
674 if (aController) { |
|
675 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_ALREADY_DISCONNECTED)); |
|
676 } |
|
677 return; |
|
678 } |
|
679 |
|
680 MOZ_ASSERT(!mDeviceAddress.IsEmpty()); |
|
681 |
|
682 mController = aController; |
|
683 |
|
684 if (!sBtA2dpInterface) { |
|
685 BT_LOGR("sBluetoothA2dpInterface is null"); |
|
686 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_NO_AVAILABLE_RESOURCE)); |
|
687 return; |
|
688 } |
|
689 |
|
690 bt_bdaddr_t remoteAddress; |
|
691 StringToBdAddressType(mDeviceAddress, &remoteAddress); |
|
692 |
|
693 bt_status_t result = sBtA2dpInterface->disconnect(&remoteAddress); |
|
694 if (BT_STATUS_SUCCESS != result) { |
|
695 BT_LOGR("Failed to disconnect: %x", result); |
|
696 aController->NotifyCompletion(NS_LITERAL_STRING(ERR_DISCONNECTION_FAILED)); |
|
697 return; |
|
698 } |
|
699 } |
|
700 |
|
701 void |
|
702 BluetoothA2dpManager::OnConnect(const nsAString& aErrorStr) |
|
703 { |
|
704 MOZ_ASSERT(NS_IsMainThread()); |
|
705 |
|
706 /** |
|
707 * On the one hand, notify the controller that we've done for outbound |
|
708 * connections. On the other hand, we do nothing for inbound connections. |
|
709 */ |
|
710 NS_ENSURE_TRUE_VOID(mController); |
|
711 |
|
712 nsRefPtr<BluetoothProfileController> controller = mController.forget(); |
|
713 controller->NotifyCompletion(aErrorStr); |
|
714 } |
|
715 |
|
716 void |
|
717 BluetoothA2dpManager::OnDisconnect(const nsAString& aErrorStr) |
|
718 { |
|
719 MOZ_ASSERT(NS_IsMainThread()); |
|
720 |
|
721 /** |
|
722 * On the one hand, notify the controller that we've done for outbound |
|
723 * connections. On the other hand, we do nothing for inbound connections. |
|
724 */ |
|
725 NS_ENSURE_TRUE_VOID(mController); |
|
726 |
|
727 nsRefPtr<BluetoothProfileController> controller = mController.forget(); |
|
728 controller->NotifyCompletion(aErrorStr); |
|
729 |
|
730 Reset(); |
|
731 } |
|
732 |
|
733 /* HandleSinkPropertyChanged update sink state in A2dp |
|
734 * |
|
735 * Possible values: "disconnected", "connecting", "connected", "playing" |
|
736 * |
|
737 * 1. "disconnected" -> "connecting" |
|
738 * Either an incoming or outgoing connection attempt ongoing |
|
739 * 2. "connecting" -> "disconnected" |
|
740 * Connection attempt failed |
|
741 * 3. "connecting" -> "connected" |
|
742 * Successfully connected |
|
743 * 4. "connected" -> "playing" |
|
744 * Audio stream active |
|
745 * 5. "playing" -> "connected" |
|
746 * Audio stream suspended |
|
747 * 6. "connected" -> "disconnected" |
|
748 * "playing" -> "disconnected" |
|
749 * Disconnected from local or the remote device |
|
750 */ |
|
751 void |
|
752 BluetoothA2dpManager::HandleSinkPropertyChanged(const BluetoothSignal& aSignal) |
|
753 { |
|
754 MOZ_ASSERT(NS_IsMainThread()); |
|
755 MOZ_ASSERT(aSignal.value().type() == |
|
756 BluetoothValue::TArrayOfBluetoothNamedValue); |
|
757 |
|
758 const nsString& address = aSignal.path(); |
|
759 /** |
|
760 * Update sink property only if |
|
761 * - mDeviceAddress is empty (A2dp is disconnected), or |
|
762 * - this property change is from the connected sink. |
|
763 */ |
|
764 NS_ENSURE_TRUE_VOID(mDeviceAddress.IsEmpty() || |
|
765 mDeviceAddress.Equals(address)); |
|
766 |
|
767 const InfallibleTArray<BluetoothNamedValue>& arr = |
|
768 aSignal.value().get_ArrayOfBluetoothNamedValue(); |
|
769 MOZ_ASSERT(arr.Length() == 1); |
|
770 |
|
771 /** |
|
772 * There are three properties: |
|
773 * - "State": a string |
|
774 * - "Connected": a boolean value |
|
775 * - "Playing": a boolean value |
|
776 * |
|
777 * Note that only "State" is handled in this function. |
|
778 */ |
|
779 |
|
780 const nsString& name = arr[0].name(); |
|
781 NS_ENSURE_TRUE_VOID(name.EqualsLiteral("State")); |
|
782 |
|
783 const BluetoothValue& value = arr[0].value(); |
|
784 MOZ_ASSERT(value.type() == BluetoothValue::TnsString); |
|
785 SinkState newState = StatusStringToSinkState(value.get_nsString()); |
|
786 NS_ENSURE_TRUE_VOID((newState != SinkState::SINK_UNKNOWN) && |
|
787 (newState != mSinkState)); |
|
788 |
|
789 SinkState prevState = mSinkState; |
|
790 mSinkState = newState; |
|
791 |
|
792 switch(mSinkState) { |
|
793 case SinkState::SINK_CONNECTING: |
|
794 // case 1: Either an incoming or outgoing connection attempt ongoing |
|
795 MOZ_ASSERT(prevState == SinkState::SINK_DISCONNECTED); |
|
796 break; |
|
797 case SinkState::SINK_PLAYING: |
|
798 // case 4: Audio stream active |
|
799 MOZ_ASSERT(prevState == SinkState::SINK_CONNECTED); |
|
800 break; |
|
801 case SinkState::SINK_CONNECTED: |
|
802 // case 5: Audio stream suspended |
|
803 if (prevState == SinkState::SINK_PLAYING || |
|
804 prevState == SinkState::SINK_CONNECTED) { |
|
805 break; |
|
806 } |
|
807 |
|
808 // case 3: Successfully connected |
|
809 mA2dpConnected = true; |
|
810 mDeviceAddress = address; |
|
811 NotifyConnectionStatusChanged(); |
|
812 |
|
813 OnConnect(EmptyString()); |
|
814 break; |
|
815 case SinkState::SINK_DISCONNECTED: |
|
816 // case 2: Connection attempt failed |
|
817 if (prevState == SinkState::SINK_CONNECTING) { |
|
818 OnConnect(NS_LITERAL_STRING(ERR_CONNECTION_FAILED)); |
|
819 break; |
|
820 } |
|
821 |
|
822 // case 6: Disconnected from the remote device |
|
823 MOZ_ASSERT(prevState == SinkState::SINK_CONNECTED || |
|
824 prevState == SinkState::SINK_PLAYING) ; |
|
825 |
|
826 mA2dpConnected = false; |
|
827 NotifyConnectionStatusChanged(); |
|
828 mDeviceAddress.Truncate(); |
|
829 OnDisconnect(EmptyString()); |
|
830 break; |
|
831 default: |
|
832 break; |
|
833 } |
|
834 } |
|
835 |
|
836 void |
|
837 BluetoothA2dpManager::NotifyConnectionStatusChanged() |
|
838 { |
|
839 MOZ_ASSERT(NS_IsMainThread()); |
|
840 |
|
841 // Notify Gecko observers |
|
842 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); |
|
843 NS_ENSURE_TRUE_VOID(obs); |
|
844 |
|
845 if (NS_FAILED(obs->NotifyObservers(this, |
|
846 BLUETOOTH_A2DP_STATUS_CHANGED_ID, |
|
847 mDeviceAddress.get()))) { |
|
848 BT_WARNING("Failed to notify bluetooth-a2dp-status-changed observsers!"); |
|
849 } |
|
850 |
|
851 // Dispatch an event of status change |
|
852 DispatchStatusChangedEvent( |
|
853 NS_LITERAL_STRING(A2DP_STATUS_CHANGED_ID), mDeviceAddress, mA2dpConnected); |
|
854 } |
|
855 |
|
856 void |
|
857 BluetoothA2dpManager::OnGetServiceChannel(const nsAString& aDeviceAddress, |
|
858 const nsAString& aServiceUuid, |
|
859 int aChannel) |
|
860 { |
|
861 } |
|
862 |
|
863 void |
|
864 BluetoothA2dpManager::OnUpdateSdpRecords(const nsAString& aDeviceAddress) |
|
865 { |
|
866 } |
|
867 |
|
868 void |
|
869 BluetoothA2dpManager::GetAddress(nsAString& aDeviceAddress) |
|
870 { |
|
871 aDeviceAddress = mDeviceAddress; |
|
872 } |
|
873 |
|
874 bool |
|
875 BluetoothA2dpManager::IsConnected() |
|
876 { |
|
877 return mA2dpConnected; |
|
878 } |
|
879 |
|
880 /* |
|
881 * In bluedroid stack case, there is no interface to know exactly |
|
882 * avrcp connection status. All connection are managed by bluedroid stack. |
|
883 */ |
|
884 void |
|
885 BluetoothA2dpManager::SetAvrcpConnected(bool aConnected) |
|
886 { |
|
887 mAvrcpConnected = aConnected; |
|
888 if (!aConnected) { |
|
889 ResetAvrcp(); |
|
890 } |
|
891 } |
|
892 |
|
893 bool |
|
894 BluetoothA2dpManager::IsAvrcpConnected() |
|
895 { |
|
896 return mAvrcpConnected; |
|
897 } |
|
898 |
|
899 /* |
|
900 * This function only updates meta data in BluetoothA2dpManager |
|
901 * Send "Get Element Attributes response" in AvrcpGetElementAttrCallback |
|
902 */ |
|
903 void |
|
904 BluetoothA2dpManager::UpdateMetaData(const nsAString& aTitle, |
|
905 const nsAString& aArtist, |
|
906 const nsAString& aAlbum, |
|
907 uint64_t aMediaNumber, |
|
908 uint64_t aTotalMediaCount, |
|
909 uint32_t aDuration) |
|
910 { |
|
911 MOZ_ASSERT(NS_IsMainThread()); |
|
912 |
|
913 #if ANDROID_VERSION > 17 |
|
914 NS_ENSURE_TRUE_VOID(sBtAvrcpInterface); |
|
915 |
|
916 // Send track changed and position changed if track num is not the same. |
|
917 // See also AVRCP 1.3 Spec 5.4.2 |
|
918 if (mMediaNumber != aMediaNumber && |
|
919 mTrackChangedNotifyType == BTRC_NOTIFICATION_TYPE_INTERIM) { |
|
920 btrc_register_notification_t param; |
|
921 // convert to network big endian format |
|
922 // since track stores as uint8[8] |
|
923 // 56 = 8 * (BTRC_UID_SIZE -1) |
|
924 for (int i = 0; i < BTRC_UID_SIZE; ++i) { |
|
925 param.track[i] = (aMediaNumber >> (56 - 8 * i)); |
|
926 } |
|
927 mTrackChangedNotifyType = BTRC_NOTIFICATION_TYPE_CHANGED; |
|
928 sBtAvrcpInterface->register_notification_rsp(BTRC_EVT_TRACK_CHANGE, |
|
929 BTRC_NOTIFICATION_TYPE_CHANGED, |
|
930 ¶m); |
|
931 if (mPlayPosChangedNotifyType == BTRC_NOTIFICATION_TYPE_INTERIM) { |
|
932 param.song_pos = mPosition; |
|
933 // EVENT_PLAYBACK_POS_CHANGED shall be notified if changed current track |
|
934 mPlayPosChangedNotifyType = BTRC_NOTIFICATION_TYPE_CHANGED; |
|
935 sBtAvrcpInterface->register_notification_rsp( |
|
936 BTRC_EVT_PLAY_POS_CHANGED, |
|
937 BTRC_NOTIFICATION_TYPE_CHANGED, |
|
938 ¶m); |
|
939 } |
|
940 } |
|
941 |
|
942 mTitle.Assign(aTitle); |
|
943 mArtist.Assign(aArtist); |
|
944 mAlbum.Assign(aAlbum); |
|
945 mMediaNumber = aMediaNumber; |
|
946 mTotalMediaCount = aTotalMediaCount; |
|
947 mDuration = aDuration; |
|
948 #endif |
|
949 } |
|
950 |
|
951 /* |
|
952 * This function is to reply AvrcpGetPlayStatusCallback (play-status-request) |
|
953 * from media player application (Gaia side) |
|
954 */ |
|
955 void |
|
956 BluetoothA2dpManager::UpdatePlayStatus(uint32_t aDuration, |
|
957 uint32_t aPosition, |
|
958 ControlPlayStatus aPlayStatus) |
|
959 { |
|
960 MOZ_ASSERT(NS_IsMainThread()); |
|
961 |
|
962 #if ANDROID_VERSION > 17 |
|
963 NS_ENSURE_TRUE_VOID(sBtAvrcpInterface); |
|
964 // always update playstatus first |
|
965 sBtAvrcpInterface->get_play_status_rsp((btrc_play_status_t)aPlayStatus, |
|
966 aDuration, aPosition); |
|
967 // when play status changed, send both play status and position |
|
968 if (mPlayStatus != aPlayStatus && |
|
969 mPlayStatusChangedNotifyType == BTRC_NOTIFICATION_TYPE_INTERIM) { |
|
970 btrc_register_notification_t param; |
|
971 param.play_status = (btrc_play_status_t)aPlayStatus; |
|
972 mPlayStatusChangedNotifyType = BTRC_NOTIFICATION_TYPE_CHANGED; |
|
973 sBtAvrcpInterface->register_notification_rsp(BTRC_EVT_PLAY_STATUS_CHANGED, |
|
974 BTRC_NOTIFICATION_TYPE_CHANGED, |
|
975 ¶m); |
|
976 } |
|
977 |
|
978 if (mPosition != aPosition && |
|
979 mPlayPosChangedNotifyType == BTRC_NOTIFICATION_TYPE_INTERIM) { |
|
980 btrc_register_notification_t param; |
|
981 param.song_pos = aPosition; |
|
982 mPlayPosChangedNotifyType = BTRC_NOTIFICATION_TYPE_CHANGED; |
|
983 sBtAvrcpInterface->register_notification_rsp(BTRC_EVT_PLAY_POS_CHANGED, |
|
984 BTRC_NOTIFICATION_TYPE_CHANGED, |
|
985 ¶m); |
|
986 } |
|
987 |
|
988 mDuration = aDuration; |
|
989 mPosition = aPosition; |
|
990 mPlayStatus = aPlayStatus; |
|
991 #endif |
|
992 } |
|
993 |
|
994 /* |
|
995 * This function handles RegisterNotification request from |
|
996 * AvrcpRegisterNotificationCallback, which updates current |
|
997 * track/status/position status in the INTERRIM response. |
|
998 * |
|
999 * aParam is only valid when position changed |
|
1000 */ |
|
1001 void |
|
1002 BluetoothA2dpManager::UpdateRegisterNotification(int aEventId, int aParam) |
|
1003 { |
|
1004 MOZ_ASSERT(NS_IsMainThread()); |
|
1005 |
|
1006 #if ANDROID_VERSION > 17 |
|
1007 NS_ENSURE_TRUE_VOID(sBtAvrcpInterface); |
|
1008 |
|
1009 btrc_register_notification_t param; |
|
1010 |
|
1011 switch (aEventId) { |
|
1012 case BTRC_EVT_PLAY_STATUS_CHANGED: |
|
1013 mPlayStatusChangedNotifyType = BTRC_NOTIFICATION_TYPE_INTERIM; |
|
1014 param.play_status = (btrc_play_status_t)mPlayStatus; |
|
1015 break; |
|
1016 case BTRC_EVT_TRACK_CHANGE: |
|
1017 // In AVRCP 1.3 and 1.4, the identifier parameter of EVENT_TRACK_CHANGED |
|
1018 // is different. |
|
1019 // AVRCP 1.4: If no track is selected, we shall return 0xFFFFFFFFFFFFFFFF, |
|
1020 // otherwise return 0x0 in the INTERRIM response. The expanded text in |
|
1021 // version 1.4 is to allow for new UID feature. As for AVRCP 1.3, we shall |
|
1022 // return 0xFFFFFFFF. Since PTS enforces to check this part to comply with |
|
1023 // the most updated spec. |
|
1024 mTrackChangedNotifyType = BTRC_NOTIFICATION_TYPE_INTERIM; |
|
1025 // needs to convert to network big endian format since track stores |
|
1026 // as uint8[8]. 56 = 8 * (BTRC_UID_SIZE -1). |
|
1027 for (int index = 0; index < BTRC_UID_SIZE; ++index) { |
|
1028 // We cannot easily check if a track is selected, so whenever A2DP is |
|
1029 // streaming, we assume a track is selected. |
|
1030 if (mSinkState == BluetoothA2dpManager::SinkState::SINK_PLAYING) { |
|
1031 param.track[index] = 0x0; |
|
1032 } else { |
|
1033 param.track[index] = 0xFF; |
|
1034 } |
|
1035 } |
|
1036 break; |
|
1037 case BTRC_EVT_PLAY_POS_CHANGED: |
|
1038 // If no track is selected, return 0xFFFFFFFF in the INTERIM response |
|
1039 mPlayPosChangedNotifyType = BTRC_NOTIFICATION_TYPE_INTERIM; |
|
1040 if (mSinkState == BluetoothA2dpManager::SinkState::SINK_PLAYING) { |
|
1041 param.song_pos = mPosition; |
|
1042 } else { |
|
1043 param.song_pos = 0xFFFFFFFF; |
|
1044 } |
|
1045 mPlaybackInterval = aParam; |
|
1046 break; |
|
1047 default: |
|
1048 break; |
|
1049 } |
|
1050 |
|
1051 sBtAvrcpInterface->register_notification_rsp((btrc_event_id_t)aEventId, |
|
1052 BTRC_NOTIFICATION_TYPE_INTERIM, |
|
1053 ¶m); |
|
1054 #endif |
|
1055 } |
|
1056 |
|
1057 void |
|
1058 BluetoothA2dpManager::GetAlbum(nsAString& aAlbum) |
|
1059 { |
|
1060 aAlbum.Assign(mAlbum); |
|
1061 } |
|
1062 |
|
1063 uint32_t |
|
1064 BluetoothA2dpManager::GetDuration() |
|
1065 { |
|
1066 return mDuration; |
|
1067 } |
|
1068 |
|
1069 ControlPlayStatus |
|
1070 BluetoothA2dpManager::GetPlayStatus() |
|
1071 { |
|
1072 return mPlayStatus; |
|
1073 } |
|
1074 |
|
1075 uint32_t |
|
1076 BluetoothA2dpManager::GetPosition() |
|
1077 { |
|
1078 return mPosition; |
|
1079 } |
|
1080 |
|
1081 uint64_t |
|
1082 BluetoothA2dpManager::GetMediaNumber() |
|
1083 { |
|
1084 return mMediaNumber; |
|
1085 } |
|
1086 |
|
1087 uint64_t |
|
1088 BluetoothA2dpManager::GetTotalMediaNumber() |
|
1089 { |
|
1090 return mTotalMediaCount; |
|
1091 } |
|
1092 |
|
1093 void |
|
1094 BluetoothA2dpManager::GetTitle(nsAString& aTitle) |
|
1095 { |
|
1096 aTitle.Assign(mTitle); |
|
1097 } |
|
1098 |
|
1099 void |
|
1100 BluetoothA2dpManager::GetArtist(nsAString& aArtist) |
|
1101 { |
|
1102 aArtist.Assign(mArtist); |
|
1103 } |
|
1104 |
|
1105 NS_IMPL_ISUPPORTS(BluetoothA2dpManager, nsIObserver) |
|
1106 |