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