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