1 /*
2  * Copyright (C) 2020 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.graphics.Bitmap;
20 import android.util.Log;
21 
22 import com.android.bluetooth.audio_util.Image;
23 import com.android.bluetooth.avrcpcontroller.BipEncoding;
24 import com.android.bluetooth.avrcpcontroller.BipImageDescriptor;
25 import com.android.bluetooth.avrcpcontroller.BipImageFormat;
26 import com.android.bluetooth.avrcpcontroller.BipImageProperties;
27 import com.android.bluetooth.avrcpcontroller.BipPixel;
28 
29 import java.io.ByteArrayOutputStream;
30 import java.security.MessageDigest;
31 import java.security.NoSuchAlgorithmException;
32 
33 /**
34  * An object to represent a piece of cover artwork/
35  *
36  * This object abstracts away the actual storage method and provides a means for others to
37  * understand available formats and get the underlying image in a particular format.
38  *
39  * All return values are ready to use by a BIP server.
40  */
41 public class CoverArt {
42     private static final String TAG = "CoverArt";
43     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
44     private static final BipPixel PIXEL_THUMBNAIL = BipPixel.createFixed(200, 200);
45 
46     private String mImageHandle = null;
47     private Bitmap mImage = null;
48 
49     /**
50      * Create a CoverArt object from an audio_util Image abstraction
51      */
CoverArt(Image image)52     CoverArt(Image image) {
53         // Create a scaled version of the image for now, as consumers don't need
54         // anything larger than this at the moment. Also makes each image gathered
55         // the same dimensions for hashing purposes.
56         mImage = Bitmap.createScaledBitmap(image.getImage(), 200, 200, false);
57     }
58 
59     /**
60      * Get the image handle that has been associated with this image.
61      *
62      * If this returns null then you will fail to generate image properties
63      */
getImageHandle()64     public String getImageHandle() {
65         return mImageHandle;
66     }
67 
68     /**
69      * Set the image handle that has been associated with this image.
70      *
71      * This is required to generate image properties
72      */
setImageHandle(String handle)73     public void setImageHandle(String handle) {
74         mImageHandle = handle;
75     }
76 
77     /**
78      * Covert a Bitmap to a byte array with an image format without lossy compression
79      */
toByteArray(Bitmap bitmap)80     private byte[] toByteArray(Bitmap bitmap) {
81         if (bitmap == null) return null;
82         ByteArrayOutputStream buffer = new ByteArrayOutputStream(
83                     bitmap.getWidth() * bitmap.getHeight());
84         bitmap.compress(Bitmap.CompressFormat.PNG, 100, buffer);
85         return buffer.toByteArray();
86     }
87 
88     /**
89      * Get a hash code of this CoverArt image
90      */
getImageHash()91     public String getImageHash() {
92         byte[] image = toByteArray(mImage);
93         if (image == null) return null;
94         String hash = null;
95         try {
96             final byte[] digestBytes;
97             final MessageDigest digest = MessageDigest.getInstance("MD5");
98             digest.update(/* Bitmap to input stream */ image);
99             byte[] messageDigest = digest.digest();
100 
101             StringBuffer hexString = new StringBuffer();
102             for (int i = 0; i < messageDigest.length; i++) {
103                 hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
104             }
105             hash = hexString.toString();
106         } catch (NoSuchAlgorithmException e) {
107             Log.e(TAG, "Failed to hash bitmap", e);
108         }
109         return hash;
110     }
111 
112     /**
113      * Get the cover artwork image bytes in the native format
114      */
getImage()115     public byte[] getImage() {
116         debug("GetImage(native)");
117         if (mImage == null) return null;
118         byte[] bytes = null;
119         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
120         mImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
121         bytes = outputStream.toByteArray();
122         return bytes;
123     }
124 
125     /**
126      * Get the cover artwork image bytes in the given encoding and pixel size
127      */
getImage(BipImageDescriptor descriptor)128     public byte[] getImage(BipImageDescriptor descriptor) {
129         debug("GetImage(descriptor=" + descriptor);
130         if (mImage == null) return null;
131         if (descriptor == null) return getImage();
132         if (!isDescriptorValid(descriptor)) {
133             error("Given format isn't available for this image");
134             return null;
135         }
136 
137         byte[] bytes = null;
138         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
139         mImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
140         bytes = outputStream.toByteArray();
141         return bytes;
142     }
143 
144     /**
145      * Determine if a given image descriptor is valid
146      */
isDescriptorValid(BipImageDescriptor descriptor)147     private boolean isDescriptorValid(BipImageDescriptor descriptor) {
148         debug("isDescriptorValid(descriptor=" + descriptor + ")");
149         if (descriptor == null) return false;
150 
151         BipEncoding encoding = descriptor.getEncoding();
152         BipPixel pixel = descriptor.getPixel();
153 
154         if (encoding.getType() == BipEncoding.JPEG && PIXEL_THUMBNAIL.equals(pixel)) {
155             return true;
156         }
157         return false;
158     }
159 
160     /**
161      * Get the cover artwork image bytes as a 200 x 200 JPEG thumbnail
162      */
getThumbnail()163     public byte[] getThumbnail() {
164         debug("GetImageThumbnail()");
165         if (mImage == null) return null;
166         byte[] bytes = null;
167         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
168         mImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
169         bytes = outputStream.toByteArray();
170         return bytes;
171     }
172 
173     /**
174      * Get the set of image properties that the cover artwork can be turned into
175      */
getImageProperties()176     public BipImageProperties getImageProperties() {
177         debug("GetImageProperties()");
178         if (mImage == null) {
179             error("Can't associate properties with a null image");
180             return null;
181         }
182         if (mImageHandle == null) {
183             error("No handle has been associated with this image. Cannot build properties.");
184             return null;
185         }
186         BipImageProperties.Builder builder = new BipImageProperties.Builder();
187         BipEncoding encoding = new BipEncoding(BipEncoding.JPEG);
188         BipPixel pixel = BipPixel.createFixed(200, 200);
189         BipImageFormat format = BipImageFormat.createNative(encoding, pixel, -1);
190 
191         builder.setImageHandle(mImageHandle);
192         builder.addNativeFormat(format);
193 
194         BipImageProperties properties = builder.build();
195         return properties;
196     }
197 
198     /**
199      * Get the storage size of this image in bytes
200      */
size()201     public int size() {
202         return mImage != null ? mImage.getAllocationByteCount() : 0;
203     }
204 
205     @Override
toString()206     public String toString() {
207         return "{handle=" + mImageHandle + ", size=" + size() + " }";
208     }
209 
210     /**
211      * Print a message to DEBUG if debug output is enabled
212      */
debug(String msg)213     private void debug(String msg) {
214         if (DEBUG) {
215             Log.d(TAG, msg);
216         }
217     }
218 
219     /**
220      * Print a message to ERROR
221      */
error(String msg)222     private void error(String msg) {
223         Log.e(TAG, msg);
224     }
225 }
226