1 /* 2 * Copyright 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.util.Log; 20 21 import java.util.HashMap; 22 import java.util.LinkedHashMap; 23 import java.util.Map; 24 25 /** 26 * A class abstracting the storage method of cover art images 27 */ 28 final class AvrcpCoverArtStorage { 29 private static final String TAG = "AvrcpCoverArtStorage"; 30 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 31 32 private final Object mHandlesLock = new Object(); 33 private int mNextImageHandle = 0; 34 35 private final Object mImagesLock = new Object(); 36 private final int mMaxImages; 37 private final Map<String, String> mImageHandles; 38 private final Map<String, CoverArt> mImages; 39 40 /** 41 * Make an image storage object with no bounds on the amount of images it can store 42 */ AvrcpCoverArtStorage()43 AvrcpCoverArtStorage() { 44 this(0); 45 } 46 47 /** 48 * Make an image storage object with a bound on the amount of images it can store 49 */ AvrcpCoverArtStorage(int maxSize)50 AvrcpCoverArtStorage(int maxSize) { 51 if (maxSize < 0) { 52 throw new IllegalArgumentException("maxSize < 0"); 53 } 54 mMaxImages = maxSize; 55 56 mImageHandles = new HashMap<String, String>(); 57 58 // Using a LinkedHashMap allows us to having items ordered LRU -> MRU (true param does this) 59 // This way, if we need run out of space we can remove from the front to remove the least 60 // recently accessed items 61 mImages = new LinkedHashMap<String, CoverArt>(0, 0.75f /* default load factor */, true); 62 } 63 64 /** 65 * Store an image and get the image handle it's been associated with. 66 */ storeImage(CoverArt coverArt)67 public String storeImage(CoverArt coverArt) { 68 debug("storeImage(CoverArt='" + coverArt + "')"); 69 if (coverArt == null || coverArt.getImage() == null) { 70 debug("Received a null image"); 71 return null; 72 } 73 74 String imageHandle = null; 75 String hash = coverArt.getImageHash(); 76 if (hash == null) { 77 error("Failed to get the hash of the image"); 78 return null; 79 } 80 81 synchronized (mImagesLock) { 82 if (mImageHandles.containsKey(hash)) { 83 debug("Already have image of hash '" + hash + "'"); 84 imageHandle = mImageHandles.get(hash); 85 debug("Sending back existing handle '" + imageHandle + "'"); 86 return imageHandle; 87 } else { 88 debug("Got a new image, hash='" + hash + "'"); 89 imageHandle = getNextImageHandle(); 90 if (imageHandle != null) { 91 mImageHandles.put(hash, imageHandle); 92 } 93 } 94 95 if (imageHandle != null) { 96 debug("Image " + coverArt + " stored at handle '" + imageHandle + "'"); 97 coverArt.setImageHandle(imageHandle); 98 mImages.put(imageHandle, coverArt); 99 trimToSize(); 100 } else { 101 error("Failed to store image. Could not get a handle."); 102 } 103 } 104 return imageHandle; 105 } 106 107 /** 108 * Get the image stored at the given image handle, if it exists 109 */ getImage(String imageHandle)110 public CoverArt getImage(String imageHandle) { 111 debug("getImage(" + imageHandle + ")"); 112 if (imageHandle == null) return null; 113 synchronized (mImagesLock) { 114 CoverArt coverArt = mImages.get(imageHandle); 115 debug("Image handle '" + imageHandle + "' -> " + coverArt); 116 return coverArt; 117 } 118 } 119 120 /** 121 * Clear out all stored images and image handles 122 */ clear()123 public void clear() { 124 synchronized (mImagesLock) { 125 mImages.clear(); 126 mImageHandles.clear(); 127 } 128 129 synchronized (mHandlesLock) { 130 mNextImageHandle = 0; 131 } 132 } 133 trimToSize()134 private void trimToSize() { 135 if (mMaxImages <= 0) return; 136 synchronized (mImagesLock) { 137 while (mImages.size() > mMaxImages) { 138 Map.Entry<String, CoverArt> entry = mImages.entrySet().iterator().next(); 139 String imageHandle = entry.getKey(); 140 CoverArt coverArt = entry.getValue(); 141 debug("Evicting '" + imageHandle + "' -> " + coverArt); 142 mImages.remove(imageHandle); 143 mImageHandles.remove(coverArt.getImageHash()); 144 } 145 } 146 } 147 148 /** 149 * Get the next available image handle value if one is available. 150 * 151 * Values are integers in the domain [0, 9999999], represented as zero-padded strings. Getting 152 * an image handle assumes you will use it. 153 */ getNextImageHandle()154 private String getNextImageHandle() { 155 synchronized (mHandlesLock) { 156 if (mNextImageHandle > 9999999) { 157 error("No more image handles left"); 158 return null; 159 } 160 161 String handle = String.valueOf(mNextImageHandle); 162 while (handle.length() != 7) { 163 handle = "0" + handle; 164 } 165 166 debug("Allocated handle " + mNextImageHandle + " --> '" + handle + "'"); 167 mNextImageHandle++; 168 return handle; 169 } 170 } 171 dump(StringBuilder sb)172 public void dump(StringBuilder sb) { 173 int bytes = 0; 174 sb.append("\n\timages (" + mImageHandles.size()); 175 if (mMaxImages > 0) sb.append(" / " + mMaxImages); 176 sb.append("):"); 177 sb.append("\n\t\tHandle : Hash : CoverArt"); 178 synchronized (mImagesLock) { 179 // Be sure to use entry set below or each access well count to the ordering 180 for (Map.Entry<String, CoverArt> entry : mImages.entrySet()) { 181 String imageHandle = entry.getKey(); 182 CoverArt coverArt = entry.getValue(); 183 String hash = "< NOT IN SET >"; 184 for (String key : mImageHandles.keySet()) { 185 String handle = mImageHandles.get(key); 186 if (imageHandle.equals(handle)) { 187 hash = key; 188 } 189 } 190 sb.append(String.format("\n\t\t%-8s : %-32s : %s", imageHandle, hash, coverArt)); 191 bytes += coverArt.size(); 192 } 193 } 194 sb.append("\n\tImage bytes: " + bytes); 195 } 196 197 /** 198 * Print a message to DEBUG if debug output is enabled 199 */ debug(String msg)200 private void debug(String msg) { 201 if (DEBUG) { 202 Log.d(TAG, msg); 203 } 204 } 205 206 /** 207 * Print a message to ERROR 208 */ error(String msg)209 private void error(String msg) { 210 Log.e(TAG, msg); 211 } 212 } 213