• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.avrcpcontroller;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothProfile;
21 import android.graphics.Bitmap;
22 import android.net.Uri;
23 import android.os.SystemProperties;
24 import android.util.Log;
25 
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.UUID;
29 import java.util.concurrent.ConcurrentHashMap;
30 
31 import javax.obex.ResponseCodes;
32 
33 /**
34  * Manager of all AVRCP Controller connections to remote devices' BIP servers for retrieving cover
35  * art.
36  *
37  * When given an image handle and device, this manager will negotiate the downloaded image
38  * properties, download the image, and place it into a Content Provider for others to retrieve from
39  */
40 public class AvrcpCoverArtManager {
41     private static final String TAG = "AvrcpCoverArtManager";
42     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
43 
44     // Image Download Schemes for cover art
45     public static final String AVRCP_CONTROLLER_COVER_ART_SCHEME =
46             "persist.bluetooth.avrcpcontroller.BIP_DOWNLOAD_SCHEME";
47     public static final String SCHEME_NATIVE = "native";
48     public static final String SCHEME_THUMBNAIL = "thumbnail";
49 
50     private final AvrcpControllerService mService;
51     protected final Map<BluetoothDevice, AvrcpBipClient> mClients = new ConcurrentHashMap<>(1);
52     private Map<BluetoothDevice, AvrcpBipSession> mBipSessions = new ConcurrentHashMap<>(1);
53     private final AvrcpCoverArtStorage mCoverArtStorage;
54     private final Callback mCallback;
55     private final String mDownloadScheme;
56 
57     /**
58      * An object representing an image download event. Contains the information necessary to
59      * retrieve the image from storage.
60      */
61     public class DownloadEvent {
62         final String mImageUuid;
63         final Uri mUri;
DownloadEvent(String uuid, Uri uri)64         public DownloadEvent(String uuid, Uri uri) {
65             mImageUuid = uuid;
66             mUri = uri;
67         }
getUuid()68         public String getUuid() {
69             return mImageUuid;
70         }
getUri()71         public Uri getUri() {
72             return mUri;
73         }
74     }
75 
76     interface Callback {
77         /**
78          * Notify of a get image download completing
79          *
80          * @param device The device the image handle belongs to
81          * @param event The download event, containing the downloaded image's information
82          */
onImageDownloadComplete(BluetoothDevice device, DownloadEvent event)83         void onImageDownloadComplete(BluetoothDevice device, DownloadEvent event);
84     }
85 
86     /**
87      * A thread-safe collection of BIP connection specific imformation meant to be cleared each
88      * time a client disconnects from the Target's BIP OBEX server.
89      *
90      * Currently contains the mapping of image handles seen to assigned UUIDs.
91      */
92     private class AvrcpBipSession {
93         private final BluetoothDevice mDevice;
94         private Map<String, String> mUuids = new ConcurrentHashMap<>(1); /* handle -> UUID */
95         private Map<String, String> mHandles = new ConcurrentHashMap<>(1); /* UUID -> handle */
96 
AvrcpBipSession(BluetoothDevice device)97         AvrcpBipSession(BluetoothDevice device) {
98             mDevice = device;
99         }
100 
getHandleUuid(String handle)101         public String getHandleUuid(String handle) {
102             if (!isValidImageHandle(handle)) return null;
103             String newUuid = UUID.randomUUID().toString();
104             String existingUuid = mUuids.putIfAbsent(handle, newUuid);
105             if (existingUuid != null) return existingUuid;
106             mHandles.put(newUuid, handle);
107             return newUuid;
108         }
109 
getUuidHandle(String uuid)110         public String getUuidHandle(String uuid) {
111             return mHandles.get(uuid);
112         }
113 
clearHandleUuids()114         public void clearHandleUuids() {
115             mUuids.clear();
116             mHandles.clear();
117         }
118 
getSessionHandles()119         public Set<String> getSessionHandles() {
120             return mUuids.keySet();
121         }
122     }
123 
124     /**
125      * Validate an image handle meets the AVRCP and BIP specifications
126      *
127      * By the BIP specification that AVRCP uses, "Image handles are 7 character long strings
128      * containing only the digits 0 to 9."
129      *
130      * @return True if the input string is a valid image handle
131      */
isValidImageHandle(String handle)132     public static boolean isValidImageHandle(String handle) {
133         if (handle == null || handle.length() != 7) return false;
134         for (char c : handle.toCharArray()) {
135             if (!Character.isDigit(c)) {
136                 return false;
137             }
138         }
139         return true;
140     }
141 
AvrcpCoverArtManager(AvrcpControllerService service, Callback callback)142     public AvrcpCoverArtManager(AvrcpControllerService service, Callback callback) {
143         mService = service;
144         mCoverArtStorage = new AvrcpCoverArtStorage(mService);
145         mCallback = callback;
146         mDownloadScheme =
147                 SystemProperties.get(AVRCP_CONTROLLER_COVER_ART_SCHEME, SCHEME_THUMBNAIL);
148         mCoverArtStorage.clear();
149     }
150 
151     /**
152      * Create a client and connect to a remote device's BIP Image Pull Server
153      *
154      * @param device The remote Bluetooth device you wish to connect to
155      * @param psm The Protocol Service Multiplexer that the remote device is hosting the server on
156      * @return True if the connection is successfully queued, False otherwise.
157      */
connect(BluetoothDevice device, int psm)158     public synchronized boolean connect(BluetoothDevice device, int psm) {
159         debug("Connect " + device.getAddress() + ", psm: " + psm);
160         if (mClients.containsKey(device)) return false;
161         AvrcpBipClient client = new AvrcpBipClient(device, psm, new BipClientCallback(device));
162         mClients.put(device, client);
163         mBipSessions.put(device, new AvrcpBipSession(device));
164         return true;
165     }
166 
167     /**
168      * Refresh the OBEX session of a connected client
169      *
170      * @param device The remote Bluetooth device you wish to refresh
171      * @return True if the refresh is successfully queued, False otherwise.
172      */
refreshSession(BluetoothDevice device)173     public synchronized boolean refreshSession(BluetoothDevice device) {
174         debug("Refresh OBEX session for " + device.getAddress());
175         AvrcpBipClient client = getClient(device);
176         if (client == null) {
177             warn("No client for " + device.getAddress());
178             return false;
179         }
180         client.refreshSession();
181         return true;
182     }
183 
184     /**
185      * Disconnect from a remote device's BIP Image Pull Server
186      *
187      * @param device The remote Bluetooth device you wish to disconnect from
188      * @return True if the disconnection is successfully queued, False otherwise.
189      */
disconnect(BluetoothDevice device)190     public synchronized boolean disconnect(BluetoothDevice device) {
191         debug("Disconnect " + device.getAddress());
192         AvrcpBipClient client = getClient(device);
193         if (client == null) {
194             warn("No client for " + device.getAddress());
195             return false;
196         }
197         client.shutdown();
198         mClients.remove(device);
199         mBipSessions.remove(device);
200         mCoverArtStorage.removeImagesForDevice(device);
201         return true;
202     }
203 
204     /**
205      * Cleanup all cover art related resources
206      *
207      * Please call when you've committed to shutting down the service.
208      */
cleanup()209     public synchronized void cleanup() {
210         debug("Clean up and shutdown");
211         for (BluetoothDevice device : mClients.keySet()) {
212             disconnect(device);
213         }
214     }
215 
216     /**
217      * Get the client connection state for a particular device's BIP Client
218      *
219      * @param device The Bluetooth device you want connection status for
220      * @return Connection status, based on BluetoothProfile.STATE_* constants
221      */
getState(BluetoothDevice device)222     public int getState(BluetoothDevice device) {
223         AvrcpBipClient client = getClient(device);
224         if (client == null) return BluetoothProfile.STATE_DISCONNECTED;
225         return client.getState();
226     }
227 
228      /**
229      * Get the UUID for an image handle coming from a particular device.
230      *
231      * This UUID is used to request and track downloads.
232      *
233      * Image handles are only good for the life of the BIP client. Since this connection is torn
234      * down frequently by specification, we have a layer of indirection to the images in the form
235      * of an UUID. This UUID will allow images to be identified outside the connection lifecycle.
236      * It also allows handles to be reused by the target in ways that won't impact image consumer's
237      * cache schemes.
238      *
239      * @param device The Bluetooth device you want a handle from
240      * @param handle The image handle you want a UUID for
241      * @return A string UUID by which the handle can be identified during the life of the BIP
242      *         connection.
243      */
getUuidForHandle(BluetoothDevice device, String handle)244     public String getUuidForHandle(BluetoothDevice device, String handle) {
245         AvrcpBipSession session = getSession(device);
246         if (session == null || !isValidImageHandle(handle)) return null;
247         return session.getHandleUuid(handle);
248     }
249 
250     /**
251      * Get the handle thats associated with a particular UUID.
252      *
253      * The handle must have been seen during this connection.
254      *
255      * @param device The Bluetooth device you want a handle from
256      * @param uuid The UUID you want the associated handle for
257      * @return The image handle associated with this UUID if it exists, null otherwise.
258      */
getHandleForUuid(BluetoothDevice device, String uuid)259     public String getHandleForUuid(BluetoothDevice device, String uuid) {
260         AvrcpBipSession session = getSession(device);
261         if (session == null || uuid == null) return null;
262         return session.getUuidHandle(uuid);
263     }
264 
clearHandleUuids(BluetoothDevice device)265     private void clearHandleUuids(BluetoothDevice device) {
266         AvrcpBipSession session = getSession(device);
267         if (session == null) return;
268         session.clearHandleUuids();
269     }
270 
271     /**
272      * Get the Uri of an image if it has already been downloaded.
273      *
274      * @param device The remote Bluetooth device you wish to get an image for
275      * @param imageUuid The UUID associated with the image you want
276      * @return A Uri the image can be found at, null if it does not exist
277      */
getImageUri(BluetoothDevice device, String imageUuid)278     public Uri getImageUri(BluetoothDevice device, String imageUuid) {
279         if (mCoverArtStorage.doesImageExist(device, imageUuid)) {
280             return AvrcpCoverArtProvider.getImageUri(device, imageUuid);
281         }
282         return null;
283     }
284 
285     /**
286      * Download an image from a remote device and make it findable via the given uri
287      *
288      * Downloading happens in three steps:
289      *   1) Get the available image formats by requesting the Image Properties
290      *   2) Determine the specific format we want the image in and turn it into an image descriptor
291      *   3) Get the image using the chosen descriptor
292      *
293      * Getting image properties and the image are both asynchronous in nature.
294      *
295      * @param device The remote Bluetooth device you wish to download from
296      * @param imageUuid The UUID associated with the image you wish to download. This will be
297      *                  translated into an image handle.
298      * @return A Uri that will be assign to the image once the download is complete
299      */
downloadImage(BluetoothDevice device, String imageUuid)300     public Uri downloadImage(BluetoothDevice device, String imageUuid) {
301         debug("Download Image - device: " + device.getAddress() + ", Handle: " + imageUuid);
302         AvrcpBipClient client = getClient(device);
303         if (client == null) {
304             error("Cannot download an image. No client is available.");
305             return null;
306         }
307 
308         // Check to see if we have the image already. No need to download it if we do have it.
309         if (mCoverArtStorage.doesImageExist(device, imageUuid)) {
310             debug("Image is already downloaded");
311             return AvrcpCoverArtProvider.getImageUri(device, imageUuid);
312         }
313 
314         // Getting image properties will return via the callback created when connecting, which
315         // invokes the download image function after we're returned the properties. If we already
316         // have the image, GetImageProperties returns true but does not start a download.
317         String imageHandle = getHandleForUuid(device, imageUuid);
318         if (imageHandle == null) {
319             warn("No handle for UUID");
320             return null;
321         }
322         boolean status = client.getImageProperties(imageHandle);
323         if (!status) return null;
324 
325         // Return the Uri that the caller should use to retrieve the image
326         return AvrcpCoverArtProvider.getImageUri(device, imageUuid);
327     }
328 
329     /**
330      * Get a specific downloaded image if it exists
331      *
332      * @param device The remote Bluetooth device associated with the image
333      * @param imageUuid The UUID associated with the image you wish to retrieve
334      */
getImage(BluetoothDevice device, String imageUuid)335     public Bitmap getImage(BluetoothDevice device, String imageUuid) {
336         return mCoverArtStorage.getImage(device, imageUuid);
337     }
338 
339     /**
340      * Remove a specific downloaded image if it exists
341      *
342      * @param device The remote Bluetooth device associated with the image
343      * @param imageUuid The UUID associated with the image you wish to remove
344      */
removeImage(BluetoothDevice device, String imageUuid)345     public void removeImage(BluetoothDevice device, String imageUuid) {
346         mCoverArtStorage.removeImage(device, imageUuid);
347     }
348 
349     /**
350      * Get a device's BIP client if it exists
351      *
352      * @param device The device you want the client for
353      * @return The AvrcpBipClient object associated with the device, or null if it doesn't exist
354      */
getClient(BluetoothDevice device)355     private AvrcpBipClient getClient(BluetoothDevice device) {
356         return mClients.get(device);
357     }
358 
359     /**
360      * Get a device's BIP session information, if it exists
361      *
362      * @param device The device you want the client for
363      * @return The AvrcpBipSession object associated with the device, or null if it doesn't exist
364      */
getSession(BluetoothDevice device)365     private AvrcpBipSession getSession(BluetoothDevice device) {
366         return mBipSessions.get(device);
367     }
368 
369     /**
370      * Determines our preferred download descriptor from the list of available image download
371      * formats presented in the image properties object.
372      *
373      * Our goal is ensure the image arrives in a format Android can consume and to minimize transfer
374      * size if possible.
375      *
376      * @param properties The set of available formats and image is downloadable in
377      * @return A descriptor containing the desirable download format
378      */
determineImageDescriptor(BipImageProperties properties)379     private BipImageDescriptor determineImageDescriptor(BipImageProperties properties) {
380         if (properties == null || !properties.isValid()) {
381             warn("Provided properties don't meet the spec. Requesting thumbnail format anyway.");
382         }
383         BipImageDescriptor.Builder builder = new BipImageDescriptor.Builder();
384         switch (mDownloadScheme) {
385             // BIP Specification says a blank/null descriptor signals to pull the native format
386             case SCHEME_NATIVE:
387                 return null;
388             // AVRCP 1.6.2 defined "thumbnail" size is guaranteed so we'll do that for now
389             case SCHEME_THUMBNAIL:
390             default:
391                 builder.setEncoding(BipEncoding.JPEG);
392                 builder.setFixedDimensions(200, 200);
393                 break;
394         }
395         return builder.build();
396     }
397 
398     /**
399      * Callback for facilitating image download
400      */
401     class BipClientCallback implements AvrcpBipClient.Callback {
402         final BluetoothDevice mDevice;
403 
BipClientCallback(BluetoothDevice device)404         BipClientCallback(BluetoothDevice device) {
405             mDevice = device;
406         }
407 
408         @Override
onConnectionStateChanged(int oldState, int newState)409         public void onConnectionStateChanged(int oldState, int newState) {
410             debug(mDevice.getAddress() + ": " + oldState + " -> " + newState);
411             if (newState == BluetoothProfile.STATE_CONNECTED) {
412                 // Ensure the handle map is cleared since old ones are invalid on a new connection
413                 clearHandleUuids(mDevice);
414 
415                 // Once we're connected fetch the current metadata again in case the target has an
416                 // image handle they can now give us. Only do this if we don't already have one.
417                 mService.getCurrentMetadataIfNoCoverArt(mDevice);
418             } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
419                 AvrcpBipClient client = getClient(mDevice);
420                 boolean shouldReconnect = (client != null);
421                 disconnect(mDevice);
422                 if (shouldReconnect) {
423                     debug("Disconnect was not expected by us. Attempt to reconnect.");
424                     connect(mDevice, client.getL2capPsm());
425                 }
426             }
427         }
428 
429         @Override
onGetImagePropertiesComplete(int status, String imageHandle, BipImageProperties properties)430         public void onGetImagePropertiesComplete(int status, String imageHandle,
431                 BipImageProperties properties) {
432             if (status != ResponseCodes.OBEX_HTTP_OK || properties == null) {
433                 warn(mDevice.getAddress() + ": GetImageProperties() failed - Handle: " + imageHandle
434                         + ", Code: " + status);
435                 return;
436             }
437             BipImageDescriptor descriptor = determineImageDescriptor(properties);
438             debug(mDevice.getAddress() + ": Download image - handle='" + imageHandle + "'");
439 
440             AvrcpBipClient client = getClient(mDevice);
441             if (client == null) {
442                 warn(mDevice.getAddress() + ": Could not getImage() for " + imageHandle
443                         + " because client has disconnected.");
444                 return;
445             }
446             client.getImage(imageHandle, descriptor);
447         }
448 
449         @Override
onGetImageComplete(int status, String imageHandle, BipImage image)450         public void onGetImageComplete(int status, String imageHandle, BipImage image) {
451             if (status != ResponseCodes.OBEX_HTTP_OK) {
452                 warn(mDevice.getAddress() + ": GetImage() failed - Handle: " + imageHandle
453                         + ", Code: " + status);
454                 return;
455             }
456             String imageUuid = getUuidForHandle(mDevice, imageHandle);
457             debug(mDevice.getAddress() + ": Received image data for handle: " + imageHandle
458                     + ", uuid: " + imageUuid + ", image: " + image);
459             Uri uri = mCoverArtStorage.addImage(mDevice, imageUuid, image.getImage());
460             if (uri == null) {
461                 error("Could not store downloaded image");
462                 return;
463             }
464             DownloadEvent event = new DownloadEvent(imageUuid, uri);
465             if (mCallback != null) mCallback.onImageDownloadComplete(mDevice, event);
466         }
467     }
468 
469     @Override
toString()470     public String toString() {
471         String s = "CoverArtManager:\n";
472         s += "    Download Scheme: " + mDownloadScheme + "\n";
473         for (BluetoothDevice device : mClients.keySet()) {
474             AvrcpBipClient client = getClient(device);
475             AvrcpBipSession session = getSession(device);
476             s += "    " + device.getAddress() + ":" + "\n";
477             s += "      Client: " + client.toString() + "\n";
478             s += "      Handles: " + "\n";
479             for (String handle : session.getSessionHandles()) {
480                 s += "        " + handle + " -> " + session.getHandleUuid(handle) + "\n";
481             }
482         }
483         s += "  " + mCoverArtStorage.toString();
484         return s;
485     }
486 
487     /**
488      * Print to debug if debug is enabled for this class
489      */
debug(String msg)490     private void debug(String msg) {
491         if (DBG) {
492             Log.d(TAG, msg);
493         }
494     }
495 
496     /**
497      * Print to warn
498      */
warn(String msg)499     private void warn(String msg) {
500         Log.w(TAG, msg);
501     }
502 
503     /**
504      * Print to error
505      */
error(String msg)506     private void error(String msg) {
507         Log.e(TAG, msg);
508     }
509 }
510