blob: c3ed4c86e97ba428391168db365e301362d78dd4 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.avrcp;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothAvrcpTarget;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Looper;
import android.os.SystemProperties;
import android.os.UserManager;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.audio_util.BTAudioEventLogger;
import com.android.bluetooth.audio_util.MediaData;
import com.android.bluetooth.audio_util.MediaPlayerList;
import com.android.bluetooth.audio_util.MediaPlayerWrapper;
import com.android.bluetooth.audio_util.Metadata;
import com.android.bluetooth.audio_util.PlayStatus;
import com.android.bluetooth.audio_util.PlayerInfo;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
import java.util.Objects;
/**
* Provides Bluetooth AVRCP Target profile as a service in the Bluetooth application.
* @hide
*/
public class AvrcpTargetService extends ProfileService {
private static final String TAG = "AvrcpTargetService";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String AVRCP_ENABLE_PROPERTY = "persist.bluetooth.enablenewavrcp";
private static final int AVRCP_MAX_VOL = 127;
private static final int MEDIA_KEY_EVENT_LOGGER_SIZE = 20;
private static final String MEDIA_KEY_EVENT_LOGGER_TITLE = "Media Key Events";
private static int sDeviceMaxVolume = 0;
private final BTAudioEventLogger mMediaKeyEventLogger = new BTAudioEventLogger(
MEDIA_KEY_EVENT_LOGGER_SIZE, MEDIA_KEY_EVENT_LOGGER_TITLE);
private AvrcpVersion mAvrcpVersion;
private MediaPlayerList mMediaPlayerList;
private AudioManager mAudioManager;
private AvrcpBroadcastReceiver mReceiver;
private AvrcpNativeInterface mNativeInterface;
private AvrcpVolumeManager mVolumeManager;
private ServiceFactory mFactory = new ServiceFactory();
// Only used to see if the metadata has changed from its previous value
private MediaData mCurrentData;
// Cover Art Service (Storage + BIP Server)
private AvrcpCoverArtService mAvrcpCoverArtService = null;
private static AvrcpTargetService sInstance = null;
class ListCallback implements MediaPlayerList.MediaUpdateCallback {
@Override
public void run(MediaData data) {
if (mNativeInterface == null) return;
boolean metadata = !Objects.equals(mCurrentData.metadata, data.metadata);
boolean state = !MediaPlayerWrapper.playstateEquals(mCurrentData.state, data.state);
boolean queue = !Objects.equals(mCurrentData.queue, data.queue);
if (DEBUG) {
Log.d(TAG, "onMediaUpdated: track_changed=" + metadata
+ " state=" + state + " queue=" + queue);
}
mCurrentData = data;
mNativeInterface.sendMediaUpdate(metadata, state, queue);
}
@Override
public void run(boolean availablePlayers, boolean addressedPlayers,
boolean uids) {
if (mNativeInterface == null) return;
mNativeInterface.sendFolderUpdate(availablePlayers, addressedPlayers, uids);
}
}
private class AvrcpBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
if (mNativeInterface == null) return;
// Update all the playback status info for each connected device
mNativeInterface.sendMediaUpdate(false, true, false);
} else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
if (mNativeInterface == null) return;
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) return;
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
if (state == BluetoothProfile.STATE_DISCONNECTED) {
// If there is no connection, disconnectDevice() will do nothing
if (mNativeInterface.disconnectDevice(device.getAddress())) {
Log.d(TAG, "request to disconnect device " + device);
}
}
} else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
if (streamType == AudioManager.STREAM_MUSIC) {
int volume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
BluetoothDevice activeDevice = getA2dpActiveDevice();
if (activeDevice != null
&& !mVolumeManager.getAbsoluteVolumeSupported(activeDevice)) {
Log.d(TAG, "stream volume change to " + volume + " " + activeDevice);
mVolumeManager.storeVolumeForDevice(activeDevice, volume);
}
}
}
}
}
/**
* Set the AvrcpTargetService instance.
*/
@VisibleForTesting
public static void set(AvrcpTargetService instance) {
sInstance = instance;
}
/**
* Get the AvrcpTargetService instance. Returns null if the service hasn't been initialized.
*/
public static AvrcpTargetService get() {
return sInstance;
}
public AvrcpCoverArtService getCoverArtService() {
return mAvrcpCoverArtService;
}
@Override
public String getName() {
return TAG;
}
@Override
protected IProfileServiceBinder initBinder() {
return new AvrcpTargetBinder(this);
}
@Override
protected void setUserUnlocked(int userId) {
Log.i(TAG, "User unlocked, initializing the service");
if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) {
Log.w(TAG, "Skipping initialization of the new AVRCP Target Player List");
sInstance = null;
return;
}
if (mMediaPlayerList != null) {
mMediaPlayerList.init(new ListCallback());
}
}
@Override
protected boolean start() {
if (sInstance != null) {
Log.wtf(TAG, "The service has already been initialized");
return false;
}
Log.i(TAG, "Starting the AVRCP Target Service");
mCurrentData = new MediaData(null, null, null);
if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) {
Log.w(TAG, "Skipping initialization of the new AVRCP Target Service");
sInstance = null;
return true;
}
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
mMediaPlayerList = new MediaPlayerList(Looper.myLooper(), this);
mNativeInterface = AvrcpNativeInterface.getInterface();
mNativeInterface.init(AvrcpTargetService.this);
mAvrcpVersion = AvrcpVersion.getCurrentSystemPropertiesValue();
mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);
UserManager userManager = UserManager.get(getApplicationContext());
if (userManager.isUserUnlocked()) {
mMediaPlayerList.init(new ListCallback());
}
if (getResources().getBoolean(R.bool.avrcp_target_enable_cover_art)) {
if (mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) {
mAvrcpCoverArtService = new AvrcpCoverArtService(this);
boolean started = mAvrcpCoverArtService.start();
if (!started) {
Log.e(TAG, "Failed to start cover art service");
mAvrcpCoverArtService = null;
}
} else {
Log.e(TAG, "Please use AVRCP version 1.6 to enable cover art");
}
}
mReceiver = new AvrcpBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
registerReceiver(mReceiver, filter);
// Only allow the service to be used once it is initialized
sInstance = this;
return true;
}
@Override
protected boolean stop() {
Log.i(TAG, "Stopping the AVRCP Target Service");
if (sInstance == null) {
Log.w(TAG, "stop() called before start()");
return true;
}
if (mAvrcpCoverArtService != null) {
mAvrcpCoverArtService.stop();
}
mAvrcpCoverArtService = null;
sInstance = null;
unregisterReceiver(mReceiver);
// We check the interfaces first since they only get set on User Unlocked
if (mMediaPlayerList != null) mMediaPlayerList.cleanup();
if (mNativeInterface != null) mNativeInterface.cleanup();
mMediaPlayerList = null;
mNativeInterface = null;
mAudioManager = null;
mReceiver = null;
return true;
}
private void init() {
}
private BluetoothDevice getA2dpActiveDevice() {
A2dpService service = mFactory.getA2dpService();
if (service == null) {
return null;
}
return service.getActiveDevice();
}
private void setA2dpActiveDevice(BluetoothDevice device) {
A2dpService service = A2dpService.getA2dpService();
if (service == null) {
Log.d(TAG, "setA2dpActiveDevice: A2dp service not found");
return;
}
service.setActiveDevice(device);
}
void deviceConnected(BluetoothDevice device, boolean absoluteVolume) {
Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume);
mVolumeManager.deviceConnected(device, absoluteVolume);
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP);
}
void deviceDisconnected(BluetoothDevice device) {
Log.i(TAG, "deviceDisconnected: device=" + device);
mVolumeManager.deviceDisconnected(device);
}
/**
* Signal to the service that the current audio out device has changed and to inform
* the audio service whether the new device supports absolute volume. If it does, also
* set the absolute volume level on the remote device.
*/
public void volumeDeviceSwitched(BluetoothDevice device) {
if (DEBUG) {
Log.d(TAG, "volumeDeviceSwitched: device=" + device);
}
mVolumeManager.volumeDeviceSwitched(device);
}
/**
* Remove the stored volume for a device.
*/
public void removeStoredVolumeForDevice(BluetoothDevice device) {
if (device == null) return;
mVolumeManager.removeStoredVolumeForDevice(device);
}
/**
* Retrieve the remembered volume for a device. Returns -1 if there is no volume for the
* device.
*/
public int getRememberedVolumeForDevice(BluetoothDevice device) {
if (device == null) return -1;
return mVolumeManager.getVolume(device, mVolumeManager.getNewDeviceVolume());
}
// TODO (apanicke): Add checks to rejectlist Absolute Volume devices if they behave poorly.
void setVolume(int avrcpVolume) {
BluetoothDevice activeDevice = getA2dpActiveDevice();
if (activeDevice == null) {
Log.d(TAG, "setVolume: no active device");
return;
}
mVolumeManager.setVolume(activeDevice, avrcpVolume);
}
/**
* Set the volume on the remote device. Does nothing if the device doesn't support absolute
* volume.
*/
public void sendVolumeChanged(int deviceVolume) {
BluetoothDevice activeDevice = getA2dpActiveDevice();
if (activeDevice == null) {
Log.d(TAG, "sendVolumeChanged: no active device");
return;
}
mVolumeManager.sendVolumeChanged(activeDevice, deviceVolume);
}
Metadata getCurrentSongInfo() {
Metadata metadata = mMediaPlayerList.getCurrentSongInfo();
if (mAvrcpCoverArtService != null && metadata.image != null) {
String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
if (imageHandle != null) metadata.image.setImageHandle(imageHandle);
}
return metadata;
}
PlayStatus getPlayState() {
return PlayStatus.fromPlaybackState(mMediaPlayerList.getCurrentPlayStatus(),
Long.parseLong(getCurrentSongInfo().duration));
}
String getCurrentMediaId() {
String id = mMediaPlayerList.getCurrentMediaId();
if (id != null) return id;
Metadata song = getCurrentSongInfo();
if (song != null) return song.mediaId;
// We always want to return something, the error string just makes debugging easier
return "error";
}
List<Metadata> getNowPlayingList() {
List<Metadata> nowPlayingList = mMediaPlayerList.getNowPlayingList();
if (mAvrcpCoverArtService != null) {
for (Metadata metadata : nowPlayingList) {
if (metadata.image != null) {
String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
if (imageHandle != null) metadata.image.setImageHandle(imageHandle);
}
}
}
return nowPlayingList;
}
int getCurrentPlayerId() {
return mMediaPlayerList.getCurrentPlayerId();
}
// TODO (apanicke): Have the Player List also contain info about the play state of each player
List<PlayerInfo> getMediaPlayerList() {
return mMediaPlayerList.getMediaPlayerList();
}
void getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb) {
mMediaPlayerList.getPlayerRoot(playerId, cb);
}
void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) {
mMediaPlayerList.getFolderItems(playerId, mediaId, cb);
}
void playItem(int playerId, boolean nowPlaying, String mediaId) {
// NOTE: playerId isn't used if nowPlaying is true, since its assumed to be the current
// active player
mMediaPlayerList.playItem(playerId, nowPlaying, mediaId);
}
// TODO (apanicke): Handle key events here in the service. Currently it was more convenient to
// handle them there but logically they make more sense handled here.
void sendMediaKeyEvent(int event, boolean pushed) {
BluetoothDevice activeDevice = getA2dpActiveDevice();
MediaPlayerWrapper player = mMediaPlayerList.getActivePlayer();
mMediaKeyEventLogger.logd(DEBUG, TAG, "getMediaKeyEvent:" + " device=" + activeDevice
+ " event=" + event + " pushed=" + pushed
+ " to " + (player == null ? null : player.getPackageName()));
mMediaPlayerList.sendMediaKeyEvent(event, pushed);
}
void setActiveDevice(BluetoothDevice device) {
Log.i(TAG, "setActiveDevice: device=" + device);
if (device == null) {
Log.wtf(TAG, "setActiveDevice: could not find device " + device);
}
setA2dpActiveDevice(device);
}
/**
* Dump debugging information to the string builder
*/
public void dump(StringBuilder sb) {
sb.append("\nProfile: AvrcpTargetService:\n");
if (sInstance == null) {
sb.append("AvrcpTargetService not running");
return;
}
StringBuilder tempBuilder = new StringBuilder();
tempBuilder.append("AVRCP version: " + mAvrcpVersion + "\n");
if (mMediaPlayerList != null) {
mMediaPlayerList.dump(tempBuilder);
} else {
tempBuilder.append("\nMedia Player List is empty\n");
}
mMediaKeyEventLogger.dump(tempBuilder);
tempBuilder.append("\n");
mVolumeManager.dump(tempBuilder);
if (mAvrcpCoverArtService != null) {
tempBuilder.append("\n");
mAvrcpCoverArtService.dump(tempBuilder);
}
// Tab everything over by two spaces
sb.append(tempBuilder.toString().replaceAll("(?m)^", " "));
}
private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub
implements IProfileServiceBinder {
private AvrcpTargetService mService;
AvrcpTargetBinder(AvrcpTargetService service) {
mService = service;
}
@Override
public void cleanup() {
mService = null;
}
@Override
public void sendVolumeChanged(int volume) {
if (!Utils.checkCaller()) {
Log.w(TAG, "sendVolumeChanged not allowed for non-active user");
return;
}
if (mService == null) {
return;
}
mService.sendVolumeChanged(volume);
}
}
}