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