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