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.SystemProperties;
30 import android.os.UserManager;
31 import android.util.Log;
32 
33 import com.android.bluetooth.BluetoothMetricsProto;
34 import com.android.bluetooth.Utils;
35 import com.android.bluetooth.a2dp.A2dpService;
36 import com.android.bluetooth.btservice.MetricsLogger;
37 import com.android.bluetooth.btservice.ProfileService;
38 import com.android.bluetooth.btservice.ServiceFactory;
39 
40 import java.util.List;
41 import java.util.Objects;
42 
43 /**
44  * Provides Bluetooth AVRCP Target profile as a service in the Bluetooth application.
45  * @hide
46  */
47 public class AvrcpTargetService extends ProfileService {
48     private static final String TAG = "AvrcpTargetService";
49     private static final boolean DEBUG = true;
50     private static final String AVRCP_ENABLE_PROPERTY = "persist.bluetooth.enablenewavrcp";
51 
52     private static final int AVRCP_MAX_VOL = 127;
53     private static final int MEDIA_KEY_EVENT_LOGGER_SIZE = 20;
54     private static final String MEDIA_KEY_EVENT_LOGGER_TITLE = "Media Key Events";
55     private static int sDeviceMaxVolume = 0;
56     private final AvrcpEventLogger mMediaKeyEventLogger = new AvrcpEventLogger(
57             MEDIA_KEY_EVENT_LOGGER_SIZE, MEDIA_KEY_EVENT_LOGGER_TITLE);
58 
59     private MediaPlayerList mMediaPlayerList;
60     private AudioManager mAudioManager;
61     private AvrcpBroadcastReceiver mReceiver;
62     private AvrcpNativeInterface mNativeInterface;
63     private AvrcpVolumeManager mVolumeManager;
64     private ServiceFactory mFactory = new ServiceFactory();
65 
66     // Only used to see if the metadata has changed from its previous value
67     private MediaData mCurrentData;
68 
69     private static AvrcpTargetService sInstance = null;
70 
71     class ListCallback implements MediaPlayerList.MediaUpdateCallback,
72             MediaPlayerList.FolderUpdateCallback {
73         @Override
run(MediaData data)74         public void run(MediaData data) {
75             if (mNativeInterface == null) return;
76 
77             boolean metadata = !Objects.equals(mCurrentData.metadata, data.metadata);
78             boolean state = !MediaPlayerWrapper.playstateEquals(mCurrentData.state, data.state);
79             boolean queue = !Objects.equals(mCurrentData.queue, data.queue);
80 
81             if (DEBUG) {
82                 Log.d(TAG, "onMediaUpdated: track_changed=" + metadata
83                         + " state=" + state + " queue=" + queue);
84             }
85             mCurrentData = data;
86 
87             mNativeInterface.sendMediaUpdate(metadata, state, queue);
88         }
89 
90         @Override
run(boolean availablePlayers, boolean addressedPlayers, boolean uids)91         public void run(boolean availablePlayers, boolean addressedPlayers,
92                 boolean uids) {
93             if (mNativeInterface == null) return;
94 
95             mNativeInterface.sendFolderUpdate(availablePlayers, addressedPlayers, uids);
96         }
97     }
98 
99     private class AvrcpBroadcastReceiver extends BroadcastReceiver {
100         @Override
onReceive(Context context, Intent intent)101         public void onReceive(Context context, Intent intent) {
102             String action = intent.getAction();
103             if (action.equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
104                 if (mNativeInterface == null) return;
105 
106                 // Update all the playback status info for each connected device
107                 mNativeInterface.sendMediaUpdate(false, true, false);
108             } else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
109                 if (mNativeInterface == null) return;
110 
111                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
112                 if (device == null) return;
113 
114                 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
115                 if (state == BluetoothProfile.STATE_DISCONNECTED) {
116                     // If there is no connection, disconnectDevice() will do nothing
117                     if (mNativeInterface.disconnectDevice(device.getAddress())) {
118                         Log.d(TAG, "request to disconnect device " + device);
119                     }
120                 }
121             } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
122                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
123                 if (streamType == AudioManager.STREAM_MUSIC) {
124                     int volume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
125                     BluetoothDevice activeDevice = getA2dpActiveDevice();
126                     if (activeDevice != null
127                             && !mVolumeManager.getAbsoluteVolumeSupported(activeDevice)) {
128                         Log.d(TAG, "stream volume change to " + volume + " " + activeDevice);
129                         mVolumeManager.storeVolumeForDevice(activeDevice, volume);
130                     }
131                 }
132             }
133         }
134     }
135 
136     /**
137      * Get the AvrcpTargetService instance. Returns null if the service hasn't been initialized.
138      */
get()139     public static AvrcpTargetService get() {
140         return sInstance;
141     }
142 
143     @Override
getName()144     public String getName() {
145         return TAG;
146     }
147 
148     @Override
initBinder()149     protected IProfileServiceBinder initBinder() {
150         return new AvrcpTargetBinder(this);
151     }
152 
153     @Override
setUserUnlocked(int userId)154     protected void setUserUnlocked(int userId) {
155         Log.i(TAG, "User unlocked, initializing the service");
156 
157         if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) {
158             Log.w(TAG, "Skipping initialization of the new AVRCP Target Player List");
159             sInstance = null;
160             return;
161         }
162 
163         if (mMediaPlayerList != null) {
164             mMediaPlayerList.init(new ListCallback());
165         }
166     }
167 
168     @Override
start()169     protected boolean start() {
170         if (sInstance != null) {
171             Log.wtf(TAG, "The service has already been initialized");
172             return false;
173         }
174 
175         Log.i(TAG, "Starting the AVRCP Target Service");
176         mCurrentData = new MediaData(null, null, null);
177 
178         if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) {
179             Log.w(TAG, "Skipping initialization of the new AVRCP Target Service");
180             sInstance = null;
181             return true;
182         }
183 
184         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
185         sDeviceMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
186 
187         mMediaPlayerList = new MediaPlayerList(Looper.myLooper(), this);
188 
189         mNativeInterface = AvrcpNativeInterface.getInterface();
190         mNativeInterface.init(AvrcpTargetService.this);
191 
192         mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);
193 
194         UserManager userManager = UserManager.get(getApplicationContext());
195         if (userManager.isUserUnlocked()) {
196             mMediaPlayerList.init(new ListCallback());
197         }
198 
199         mReceiver = new AvrcpBroadcastReceiver();
200         IntentFilter filter = new IntentFilter();
201         filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
202         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
203         filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
204         registerReceiver(mReceiver, filter);
205 
206         // Only allow the service to be used once it is initialized
207         sInstance = this;
208 
209         return true;
210     }
211 
212     @Override
stop()213     protected boolean stop() {
214         Log.i(TAG, "Stopping the AVRCP Target Service");
215 
216         if (sInstance == null) {
217             Log.w(TAG, "stop() called before start()");
218             return true;
219         }
220 
221         sInstance = null;
222         unregisterReceiver(mReceiver);
223 
224         // We check the interfaces first since they only get set on User Unlocked
225         if (mMediaPlayerList != null) mMediaPlayerList.cleanup();
226         if (mNativeInterface != null) mNativeInterface.cleanup();
227 
228         mMediaPlayerList = null;
229         mNativeInterface = null;
230         mAudioManager = null;
231         mReceiver = null;
232         return true;
233     }
234 
init()235     private void init() {
236     }
237 
getA2dpActiveDevice()238     private BluetoothDevice getA2dpActiveDevice() {
239         A2dpService service = mFactory.getA2dpService();
240         if (service == null) {
241             return null;
242         }
243         return service.getActiveDevice();
244     }
245 
setA2dpActiveDevice(BluetoothDevice device)246     private void setA2dpActiveDevice(BluetoothDevice device) {
247         A2dpService service = A2dpService.getA2dpService();
248         if (service == null) {
249             Log.d(TAG, "setA2dpActiveDevice: A2dp service not found");
250             return;
251         }
252         service.setActiveDevice(device);
253     }
254 
deviceConnected(BluetoothDevice device, boolean absoluteVolume)255     void deviceConnected(BluetoothDevice device, boolean absoluteVolume) {
256         Log.i(TAG, "deviceConnected: device=" + device + " absoluteVolume=" + absoluteVolume);
257         mVolumeManager.deviceConnected(device, absoluteVolume);
258         MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.AVRCP);
259     }
260 
deviceDisconnected(BluetoothDevice device)261     void deviceDisconnected(BluetoothDevice device) {
262         Log.i(TAG, "deviceDisconnected: device=" + device);
263         mVolumeManager.deviceDisconnected(device);
264     }
265 
266     /**
267      * Signal to the service that the current audio out device has changed and to inform
268      * the audio service whether the new device supports absolute volume. If it does, also
269      * set the absolute volume level on the remote device.
270      */
volumeDeviceSwitched(BluetoothDevice device)271     public void volumeDeviceSwitched(BluetoothDevice device) {
272         if (DEBUG) {
273             Log.d(TAG, "volumeDeviceSwitched: device=" + device);
274         }
275         mVolumeManager.volumeDeviceSwitched(device);
276     }
277 
278     /**
279      * Remove the stored volume for a device.
280      */
removeStoredVolumeForDevice(BluetoothDevice device)281     public void removeStoredVolumeForDevice(BluetoothDevice device) {
282         if (device == null) return;
283 
284         mVolumeManager.removeStoredVolumeForDevice(device);
285     }
286 
287     /**
288      * Retrieve the remembered volume for a device. Returns -1 if there is no volume for the
289      * device.
290      */
getRememberedVolumeForDevice(BluetoothDevice device)291     public int getRememberedVolumeForDevice(BluetoothDevice device) {
292         if (device == null) return -1;
293 
294         return mVolumeManager.getVolume(device, mVolumeManager.getNewDeviceVolume());
295     }
296 
297     // TODO (apanicke): Add checks to blacklist Absolute Volume devices if they behave poorly.
setVolume(int avrcpVolume)298     void setVolume(int avrcpVolume) {
299         BluetoothDevice activeDevice = getA2dpActiveDevice();
300         if (activeDevice == null) {
301             Log.d(TAG, "setVolume: no active device");
302             return;
303         }
304 
305         mVolumeManager.setVolume(activeDevice, avrcpVolume);
306     }
307 
308     /**
309      * Set the volume on the remote device. Does nothing if the device doesn't support absolute
310      * volume.
311      */
sendVolumeChanged(int deviceVolume)312     public void sendVolumeChanged(int deviceVolume) {
313         BluetoothDevice activeDevice = getA2dpActiveDevice();
314         if (activeDevice == null) {
315             Log.d(TAG, "sendVolumeChanged: no active device");
316             return;
317         }
318 
319         mVolumeManager.sendVolumeChanged(activeDevice, deviceVolume);
320     }
321 
getCurrentSongInfo()322     Metadata getCurrentSongInfo() {
323         return mMediaPlayerList.getCurrentSongInfo();
324     }
325 
getPlayState()326     PlayStatus getPlayState() {
327         return PlayStatus.fromPlaybackState(mMediaPlayerList.getCurrentPlayStatus(),
328                 Long.parseLong(getCurrentSongInfo().duration));
329     }
330 
getCurrentMediaId()331     String getCurrentMediaId() {
332         String id = mMediaPlayerList.getCurrentMediaId();
333         if (id != null) return id;
334 
335         Metadata song = getCurrentSongInfo();
336         if (song != null) return song.mediaId;
337 
338         // We always want to return something, the error string just makes debugging easier
339         return "error";
340     }
341 
getNowPlayingList()342     List<Metadata> getNowPlayingList() {
343         return mMediaPlayerList.getNowPlayingList();
344     }
345 
getCurrentPlayerId()346     int getCurrentPlayerId() {
347         return mMediaPlayerList.getCurrentPlayerId();
348     }
349 
350     // TODO (apanicke): Have the Player List also contain info about the play state of each player
getMediaPlayerList()351     List<PlayerInfo> getMediaPlayerList() {
352         return mMediaPlayerList.getMediaPlayerList();
353     }
354 
getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb)355     void getPlayerRoot(int playerId, MediaPlayerList.GetPlayerRootCallback cb) {
356         mMediaPlayerList.getPlayerRoot(playerId, cb);
357     }
358 
getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb)359     void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) {
360         mMediaPlayerList.getFolderItems(playerId, mediaId, cb);
361     }
362 
playItem(int playerId, boolean nowPlaying, String mediaId)363     void playItem(int playerId, boolean nowPlaying, String mediaId) {
364         // NOTE: playerId isn't used if nowPlaying is true, since its assumed to be the current
365         // active player
366         mMediaPlayerList.playItem(playerId, nowPlaying, mediaId);
367     }
368 
369     // TODO (apanicke): Handle key events here in the service. Currently it was more convenient to
370     // handle them there but logically they make more sense handled here.
sendMediaKeyEvent(int event, boolean pushed)371     void sendMediaKeyEvent(int event, boolean pushed) {
372         BluetoothDevice activeDevice = getA2dpActiveDevice();
373         MediaPlayerWrapper player = mMediaPlayerList.getActivePlayer();
374         mMediaKeyEventLogger.logd(DEBUG, TAG, "getMediaKeyEvent:" + " device=" + activeDevice
375                 + " event=" + event + " pushed=" + pushed
376                 + " to " + (player == null ? null : player.getPackageName()));
377         mMediaPlayerList.sendMediaKeyEvent(event, pushed);
378     }
379 
setActiveDevice(BluetoothDevice device)380     void setActiveDevice(BluetoothDevice device) {
381         Log.i(TAG, "setActiveDevice: device=" + device);
382         if (device == null) {
383             Log.wtf(TAG, "setActiveDevice: could not find device " + device);
384         }
385         setA2dpActiveDevice(device);
386     }
387 
388     /**
389      * Dump debugging information to the string builder
390      */
dump(StringBuilder sb)391     public void dump(StringBuilder sb) {
392         sb.append("\nProfile: AvrcpTargetService:\n");
393         if (sInstance == null) {
394             sb.append("AvrcpTargetService not running");
395             return;
396         }
397 
398         StringBuilder tempBuilder = new StringBuilder();
399         if (mMediaPlayerList != null) {
400             mMediaPlayerList.dump(tempBuilder);
401         } else {
402             tempBuilder.append("\nMedia Player List is empty\n");
403         }
404 
405         mMediaKeyEventLogger.dump(tempBuilder);
406         tempBuilder.append("\n");
407         mVolumeManager.dump(tempBuilder);
408 
409         // Tab everything over by two spaces
410         sb.append(tempBuilder.toString().replaceAll("(?m)^", "  "));
411     }
412 
413     private static class AvrcpTargetBinder extends IBluetoothAvrcpTarget.Stub
414             implements IProfileServiceBinder {
415         private AvrcpTargetService mService;
416 
AvrcpTargetBinder(AvrcpTargetService service)417         AvrcpTargetBinder(AvrcpTargetService service) {
418             mService = service;
419         }
420 
421         @Override
cleanup()422         public void cleanup() {
423             mService = null;
424         }
425 
426         @Override
sendVolumeChanged(int volume)427         public void sendVolumeChanged(int volume) {
428             if (!Utils.checkCaller()) {
429                 Log.w(TAG, "sendVolumeChanged not allowed for non-active user");
430                 return;
431             }
432 
433             if (mService == null) {
434                 return;
435             }
436 
437             mService.sendVolumeChanged(volume);
438         }
439     }
440 }
441