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