• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.avrcp;
18 
19 import android.bluetooth.BluetoothA2dp;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.IBluetoothAvrcpTarget;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.media.AudioManager;
28 import android.os.Looper;
29 import android.os.UserManager;
30 import android.sysprop.BluetoothProperties;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import com.android.bluetooth.BluetoothMetricsProto;
35 import com.android.bluetooth.R;
36 import com.android.bluetooth.Utils;
37 import com.android.bluetooth.a2dp.A2dpService;
38 import com.android.bluetooth.audio_util.BTAudioEventLogger;
39 import com.android.bluetooth.audio_util.MediaData;
40 import com.android.bluetooth.audio_util.MediaPlayerList;
41 import com.android.bluetooth.audio_util.MediaPlayerWrapper;
42 import com.android.bluetooth.audio_util.Metadata;
43 import com.android.bluetooth.audio_util.PlayStatus;
44 import com.android.bluetooth.audio_util.PlayerInfo;
45 import com.android.bluetooth.btservice.AdapterService;
46 import com.android.bluetooth.btservice.MetricsLogger;
47 import com.android.bluetooth.btservice.ProfileService;
48 import com.android.bluetooth.btservice.ServiceFactory;
49 import com.android.internal.annotations.VisibleForTesting;
50 
51 import java.util.List;
52 import java.util.Objects;
53 
54 /**
55  * Provides Bluetooth AVRCP Target profile as a service in the Bluetooth application.
56  * @hide
57  */
58 public class AvrcpTargetService extends ProfileService {
59     private static final String TAG = "AvrcpTargetService";
60     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
61 
62     private static final int AVRCP_MAX_VOL = 127;
63     private static final int MEDIA_KEY_EVENT_LOGGER_SIZE = 20;
64     private static final String MEDIA_KEY_EVENT_LOGGER_TITLE = "Media Key Events";
65     private static int sDeviceMaxVolume = 0;
66     private final BTAudioEventLogger mMediaKeyEventLogger = new BTAudioEventLogger(
67             MEDIA_KEY_EVENT_LOGGER_SIZE, MEDIA_KEY_EVENT_LOGGER_TITLE);
68 
69     private AvrcpVersion mAvrcpVersion;
70     private MediaPlayerList mMediaPlayerList;
71     private AudioManager mAudioManager;
72     private AvrcpBroadcastReceiver mReceiver;
73     private AvrcpNativeInterface mNativeInterface;
74     private AvrcpVolumeManager mVolumeManager;
75     private ServiceFactory mFactory = new ServiceFactory();
76 
77     // Only used to see if the metadata has changed from its previous value
78     private MediaData mCurrentData;
79 
80     // Cover Art Service (Storage + BIP Server)
81     private AvrcpCoverArtService mAvrcpCoverArtService = null;
82 
83     private static AvrcpTargetService sInstance = null;
84 
isEnabled()85     public static boolean isEnabled() {
86         return BluetoothProperties.isProfileAvrcpTargetEnabled().orElse(false);
87     }
88 
89     class ListCallback implements MediaPlayerList.MediaUpdateCallback {
90         @Override
run(MediaData data)91         public void run(MediaData data) {
92             if (mNativeInterface == null) return;
93 
94             boolean metadata = !Objects.equals(mCurrentData.metadata, data.metadata);
95             boolean state = !MediaPlayerWrapper.playstateEquals(mCurrentData.state, data.state);
96             boolean queue = !Objects.equals(mCurrentData.queue, data.queue);
97 
98             if (DEBUG) {
99                 Log.d(TAG, "onMediaUpdated: track_changed=" + metadata
100                         + " state=" + state + " queue=" + queue);
101             }
102             mCurrentData = data;
103 
104             mNativeInterface.sendMediaUpdate(metadata, state, queue);
105         }
106 
107         @Override
run(boolean availablePlayers, boolean addressedPlayers, boolean uids)108         public void run(boolean availablePlayers, boolean addressedPlayers,
109                 boolean uids) {
110             if (mNativeInterface == null) return;
111 
112             mNativeInterface.sendFolderUpdate(availablePlayers, addressedPlayers, uids);
113         }
114     }
115 
116     private class AvrcpBroadcastReceiver extends BroadcastReceiver {
117         @Override
onReceive(Context context, Intent intent)118         public void onReceive(Context context, Intent intent) {
119             String action = intent.getAction();
120             if (action.equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
121                 if (mNativeInterface == null) return;
122 
123                 // Update all the playback status info for each connected device
124                 mNativeInterface.sendMediaUpdate(false, true, false);
125             } else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
126                 if (mNativeInterface == null) return;
127 
128                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
129                 if (device == null) return;
130 
131                 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
132                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
133                     // If there is no connection, disconnectDevice() will do nothing
134                     if (mNativeInterface.disconnectDevice(device.getAddress())) {
135                         Log.d(TAG, "request to disconnect device " + device);
136                     }
137                 }
138             } else if (action.equals(AudioManager.ACTION_VOLUME_CHANGED)) {
139                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
140                 if (streamType == AudioManager.STREAM_MUSIC) {
141                     int volume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
142                     BluetoothDevice activeDevice = getA2dpActiveDevice();
143                     if (activeDevice != null
144                             && !mVolumeManager.getAbsoluteVolumeSupported(activeDevice)) {
145                         Log.d(TAG, "stream volume change to " + volume + " " + activeDevice);
146                         mVolumeManager.storeVolumeForDevice(activeDevice, volume);
147                     }
148                 }
149             }
150         }
151     }
152 
153     /**
154      * Set the AvrcpTargetService instance.
155      */
156     @VisibleForTesting
set(AvrcpTargetService instance)157     public static void set(AvrcpTargetService instance) {
158         sInstance = instance;
159     }
160 
161     /**
162      * Get the AvrcpTargetService instance. Returns null if the service hasn't been initialized.
163      */
get()164     public static AvrcpTargetService get() {
165         return sInstance;
166     }
167 
getCoverArtService()168     public AvrcpCoverArtService getCoverArtService() {
169         return mAvrcpCoverArtService;
170     }
171 
172     @Override
getName()173     public String getName() {
174         return TAG;
175     }
176 
177     @Override
initBinder()178     protected IProfileServiceBinder initBinder() {
179         return new AvrcpTargetBinder(this);
180     }
181 
182     @Override
setUserUnlocked(int userId)183     protected void setUserUnlocked(int userId) {
184         Log.i(TAG, "User unlocked, initializing the service");
185 
186         if (mMediaPlayerList != null) {
187             mMediaPlayerList.init(new ListCallback());
188         }
189     }
190 
191     @Override
start()192     protected boolean start() {
193         if (sInstance != null) {
194             Log.wtf(TAG, "The service has already been initialized");
195             return false;
196         }
197 
198         Log.i(TAG, "Starting the AVRCP Target Service");
199         mCurrentData = new MediaData(null, null, null);
200 
201         mAudioManager = getSystemService(AudioManager.class);
202         sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
203 
204         mMediaPlayerList = new MediaPlayerList(Looper.myLooper(), this);
205 
206         mNativeInterface = AvrcpNativeInterface.getInterface();
207         mNativeInterface.init(AvrcpTargetService.this);
208 
209         mAvrcpVersion = AvrcpVersion.getCurrentSystemPropertiesValue();
210 
211         mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);
212 
213         UserManager userManager = getApplicationContext().getSystemService(UserManager.class);
214         if (userManager.isUserUnlocked()) {
215             mMediaPlayerList.init(new ListCallback());
216         }
217 
218         if (getResources().getBoolean(R.bool.avrcp_target_enable_cover_art)) {
219             if (mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) {
220                 mAvrcpCoverArtService = new AvrcpCoverArtService(this);
221                 boolean started = mAvrcpCoverArtService.start();
222                 if (!started) {
223                     Log.e(TAG, "Failed to start cover art service");
224                     mAvrcpCoverArtService = null;
225                 }
226             } else {
227                 Log.e(TAG, "Please use AVRCP version 1.6 to enable cover art");
228             }
229         }
230 
231         mReceiver = new AvrcpBroadcastReceiver();
232         IntentFilter filter = new IntentFilter();
233         filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
234         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
235         filter.addAction(AudioManager.ACTION_VOLUME_CHANGED);
236         registerReceiver(mReceiver, filter);
237 
238         // Only allow the service to be used once it is initialized
239         sInstance = this;
240         BluetoothDevice activeDevice = getA2dpActiveDevice();
241         String deviceAddress = activeDevice != null ?
242                 activeDevice.getAddress() :
243                 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS;
244         AdapterService.getAdapterService().notifyActivityAttributionInfo(
245                 getAttributionSource(), deviceAddress);
246 
247         return true;
248     }
249 
250     @Override
stop()251     protected boolean stop() {
252         Log.i(TAG, "Stopping the AVRCP Target Service");
253 
254         if (sInstance == null) {
255             Log.w(TAG, "stop() called before start()");
256             return true;
257         }
258 
259         if (mAvrcpCoverArtService != null) {
260             mAvrcpCoverArtService.stop();
261         }
262         mAvrcpCoverArtService = null;
263         BluetoothDevice activeDevice = getA2dpActiveDevice();
264         String deviceAddress = activeDevice != null ?
265                 activeDevice.getAddress() :
266                 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS;
267         AdapterService.getAdapterService().notifyActivityAttributionInfo(
268                 getAttributionSource(), deviceAddress);
269 
270         sInstance = null;
271         unregisterReceiver(mReceiver);
272 
273         // We check the interfaces first since they only get set on User Unlocked
274         if (mMediaPlayerList != null) mMediaPlayerList.cleanup();
275         if (mNativeInterface != null) mNativeInterface.cleanup();
276 
277         mMediaPlayerList = null;
278         mNativeInterface = null;
279         mAudioManager = null;
280         mReceiver = null;
281         return true;
282     }
283 
init()284     private void init() {
285     }
286 
getA2dpActiveDevice()287     private BluetoothDevice getA2dpActiveDevice() {
288         A2dpService service = mFactory.getA2dpService();
289         if (service == null) {
290             return null;
291         }
292         return service.getActiveDevice();
293     }
294 
setA2dpActiveDevice(BluetoothDevice device)295     private void setA2dpActiveDevice(BluetoothDevice device) {
296         A2dpService service = A2dpService.getA2dpService();
297         if (service == null) {
298             Log.d(TAG, "setA2dpActiveDevice: A2dp service not found");
299             return;
300         }
301         service.setActiveDevice(device);
302     }
303 
deviceConnected(BluetoothDevice device, boolean absoluteVolume)304     void deviceConnected(BluetoothDevice device, boolean absoluteVolume) {
305         Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume);
306         mVolumeManager.deviceConnected(device, absoluteVolume);
307         MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP);
308     }
309 
deviceDisconnected(BluetoothDevice device)310     void deviceDisconnected(BluetoothDevice device) {
311         Log.i(TAG, "deviceDisconnected: device=" + device);
312         mVolumeManager.deviceDisconnected(device);
313     }
314 
315     /**
316      * Signal to the service that the current audio out device has changed and to inform
317      * the audio service whether the new device supports absolute volume. If it does, also
318      * set the absolute volume level on the remote device.
319      */
volumeDeviceSwitched(BluetoothDevice device)320     public void volumeDeviceSwitched(BluetoothDevice device) {
321         if (DEBUG) {
322             Log.d(TAG, "volumeDeviceSwitched: device=" + device);
323         }
324         mVolumeManager.volumeDeviceSwitched(device);
325     }
326 
327     /**
328      * Remove the stored volume for a device.
329      */
removeStoredVolumeForDevice(BluetoothDevice device)330     public void removeStoredVolumeForDevice(BluetoothDevice device) {
331         if (device == null) return;
332 
333         mVolumeManager.removeStoredVolumeForDevice(device);
334     }
335 
336     /**
337      * Retrieve the remembered volume for a device. Returns -1 if there is no volume for the
338      * device.
339      */
getRememberedVolumeForDevice(BluetoothDevice device)340     public int getRememberedVolumeForDevice(BluetoothDevice device) {
341         if (device == null) return -1;
342 
343         return mVolumeManager.getVolume(device, mVolumeManager.getNewDeviceVolume());
344     }
345 
346     // TODO (apanicke): Add checks to rejectlist Absolute Volume devices if they behave poorly.
setVolume(int avrcpVolume)347     void setVolume(int avrcpVolume) {
348         BluetoothDevice activeDevice = getA2dpActiveDevice();
349         if (activeDevice == null) {
350             Log.d(TAG, "setVolume: no active device");
351             return;
352         }
353 
354         mVolumeManager.setVolume(activeDevice, avrcpVolume);
355     }
356 
357     /**
358      * Set the volume on the remote device. Does nothing if the device doesn't support absolute
359      * volume.
360      */
sendVolumeChanged(int deviceVolume)361     public void sendVolumeChanged(int deviceVolume) {
362         BluetoothDevice activeDevice = getA2dpActiveDevice();
363         if (activeDevice == null) {
364             Log.d(TAG, "sendVolumeChanged: no active device");
365             return;
366         }
367 
368         mVolumeManager.sendVolumeChanged(activeDevice, deviceVolume);
369     }
370 
getCurrentSongInfo()371     Metadata getCurrentSongInfo() {
372         Metadata metadata = mMediaPlayerList.getCurrentSongInfo();
373         if (mAvrcpCoverArtService != null && metadata.image != null) {
374             String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
375             if (imageHandle != null) metadata.image.setImageHandle(imageHandle);
376         }
377         return metadata;
378     }
379 
getPlayState()380     PlayStatus getPlayState() {
381         return PlayStatus.fromPlaybackState(mMediaPlayerList.getCurrentPlayStatus(),
382                 Long.parseLong(getCurrentSongInfo().duration));
383     }
384 
getCurrentMediaId()385     String getCurrentMediaId() {
386         String id = mMediaPlayerList.getCurrentMediaId();
387         if (id != null && !id.isEmpty()) return id;
388 
389         Metadata song = getCurrentSongInfo();
390         if (song != null && !song.mediaId.isEmpty()) return song.mediaId;
391 
392         // We always want to return something, the error string just makes debugging easier
393         return "error";
394     }
395 
getNowPlayingList()396     List<Metadata> getNowPlayingList() {
397         String currentMediaId = getCurrentMediaId();
398         Metadata currentTrack = null;
399         String imageHandle = null;
400         List<Metadata> nowPlayingList = mMediaPlayerList.getNowPlayingList();
401         if (mAvrcpCoverArtService != null) {
402             for (Metadata metadata : nowPlayingList) {
403                 if (TextUtils.equals(metadata.mediaId, currentMediaId)) {
404                     currentTrack = metadata;
405                 } else if (metadata.image != null) {
406                     imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
407                     if (imageHandle != null) {
408                         metadata.image.setImageHandle(imageHandle);
409                     }
410                 }
411             }
412 
413             // Always store the current item from the queue last so we know the image is in storage
414             if (currentTrack != null) {
415                 imageHandle = mAvrcpCoverArtService.storeImage(currentTrack.image);
416                 if (imageHandle != null) {
417                     currentTrack.image.setImageHandle(imageHandle);
418                 }
419             }
420         }
421         return nowPlayingList;
422     }
423 
getCurrentPlayerId()424     int getCurrentPlayerId() {
425         return mMediaPlayerList.getCurrentPlayerId();
426     }
427 
428     // TODO (apanicke): Have the Player List also contain info about the play state of each player
getMediaPlayerList()429     List<PlayerInfo> getMediaPlayerList() {
430         return mMediaPlayerList.getMediaPlayerList();
431     }
432 
getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb)433     void getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb) {
434         mMediaPlayerList.getPlayerRoot(playerId, cb);
435     }
436 
getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb)437     void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) {
438         mMediaPlayerList.getFolderItems(playerId, mediaId, cb);
439     }
440 
playItem(int playerId, boolean nowPlaying, String mediaId)441     void playItem(int playerId, boolean nowPlaying, String mediaId) {
442         // NOTE: playerId isn't used if nowPlaying is true, since its assumed to be the current
443         // active player
444         mMediaPlayerList.playItem(playerId, nowPlaying, mediaId);
445     }
446 
447     // TODO (apanicke): Handle key events here in the service. Currently it was more convenient to
448     // handle them there but logically they make more sense handled here.
sendMediaKeyEvent(int event, boolean pushed)449     void sendMediaKeyEvent(int event, boolean pushed) {
450         BluetoothDevice activeDevice = getA2dpActiveDevice();
451         MediaPlayerWrapper player = mMediaPlayerList.getActivePlayer();
452         mMediaKeyEventLogger.logd(DEBUG, TAG, "getMediaKeyEvent:" + " device=" + activeDevice
453                 + " event=" + event + " pushed=" + pushed
454                 + " to " + (player == null ? null : player.getPackageName()));
455         mMediaPlayerList.sendMediaKeyEvent(event, pushed);
456     }
457 
setActiveDevice(BluetoothDevice device)458     void setActiveDevice(BluetoothDevice device) {
459         Log.i(TAG, "setActiveDevice: device=" + device);
460         if (device == null) {
461             Log.wtf(TAG, "setActiveDevice: could not find device " + device);
462         }
463         setA2dpActiveDevice(device);
464     }
465 
466     /**
467      * Dump debugging information to the string builder
468      */
dump(StringBuilder sb)469     public void dump(StringBuilder sb) {
470         sb.append("\nProfile: AvrcpTargetService:\n");
471         if (sInstance == null) {
472             sb.append("AvrcpTargetService not running");
473             return;
474         }
475 
476         StringBuilder tempBuilder = new StringBuilder();
477         tempBuilder.append("AVRCP version: " + mAvrcpVersion + "\n");
478 
479         if (mMediaPlayerList != null) {
480             mMediaPlayerList.dump(tempBuilder);
481         } else {
482             tempBuilder.append("\nMedia Player List is empty\n");
483         }
484 
485         mMediaKeyEventLogger.dump(tempBuilder);
486         tempBuilder.append("\n");
487         mVolumeManager.dump(tempBuilder);
488         if (mAvrcpCoverArtService != null) {
489             tempBuilder.append("\n");
490             mAvrcpCoverArtService.dump(tempBuilder);
491         }
492 
493         // Tab everything over by two spaces
494         sb.append(tempBuilder.toString().replaceAll("(?m)^", "  "));
495     }
496 
497     private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub
498             implements IProfileServiceBinder {
499         private AvrcpTargetService mService;
500 
AvrcpTargetBinder(AvrcpTargetService service)501         AvrcpTargetBinder(AvrcpTargetService service) {
502             mService = service;
503         }
504 
505         @Override
cleanup()506         public void cleanup() {
507             mService = null;
508         }
509 
510         @Override
sendVolumeChanged(int volume)511         public void sendVolumeChanged(int volume) {
512             if (mService == null
513                     || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG)) {
514                 return;
515             }
516 
517             mService.sendVolumeChanged(volume);
518         }
519     }
520 }
521