1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.net.URL;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Set;
29
30 import org.jajuk.util.DownloadManager;
31
32 import sk.baka.ambient.collection.CategoryEnum;
33 import sk.baka.ambient.collection.CategoryItem;
34 import sk.baka.ambient.collection.ICollection;
35 import sk.baka.ambient.collection.TrackMetadataBean;
36 import sk.baka.ambient.collection.local.LibraryCollection;
37 import sk.baka.ambient.collection.local.MediaStoreCollection;
38 import sk.baka.ambient.commons.AbstractFileStorage;
39 import sk.baka.ambient.commons.MiscUtils;
40 import sk.baka.ambient.commons.SimpleBus;
41 import sk.baka.ambient.library.ILibraryListener;
42 import android.graphics.Bitmap;
43 import android.graphics.BitmapFactory;
44 import android.widget.ImageView;
45
46 /***
47 * Provides covers and manages cover cache. Thread-safe. Essentially a
48 * singleton, there must be at most one instance.
49 *
50 * @author Martin Vysny
51 */
52 public final class CoverCache extends AbstractFileStorage implements
53 ILibraryListener {
54
55 /***
56 * Creates new cache.
57 */
58 public CoverCache() {
59 super(AmbientApplication.getInstance().getString(R.string.covers),
60 ".png", ".jpg", ".gif");
61 }
62
63 /***
64 * If <code>true</code> then new covers will not be downloaded. If
65 * <code>false</code> then old covers will be purged.
66 */
67 private boolean dontDownloadOnCacheFull = true;
68
69 /***
70 * Sets the cache behavior when the cache is full.
71 *
72 * @param dontDownload
73 * if <code>true</code> then new covers will not be downloaded.
74 * If <code>false</code> then old covers will be purged.
75 */
76 public synchronized void setCacheOverflowBehavior(final boolean dontDownload) {
77 this.dontDownloadOnCacheFull = dontDownload;
78 }
79
80 /***
81 * Fetches a cover. Attempts to fetch it from the internet if a cover is not
82 * found and we are online - in this case a background thread is run and the
83 * method returns <code>null</code>.
84 *
85 * @param track
86 * the track.
87 * @return cover or <code>null</code> if we are unable to provide any cover.
88 */
89 private File getCover(final TrackMetadataBean track) {
90 return getFile(track);
91 }
92
93 @Override
94 protected boolean isProceedWithDownload(Object fetchInfo) {
95 removeObsoleteAlbums();
96 final boolean proceed = !(dontDownloadOnCacheFull && isFull());
97 if (proceed) {
98 removeOldestCovers();
99 }
100 return proceed;
101 }
102
103 @Override
104 protected String[] getFilenameAndExt(URL url, Object fetchInfo) {
105 final TrackMetadataBean track = (TrackMetadataBean) fetchInfo;
106 if (MiscUtils.isEmptyOrWhitespace(track.getAlbum()))
107 return null;
108 final String ext = url == null ? null : "."
109 + DownloadManager.getExtension(url);
110 final String name = track.getAlbum();
111 return new String[] { name, ext };
112 }
113
114 @Override
115 protected void onFileDownloaded(URL url, Object fetchInfo,
116 final boolean success) {
117 if (!success) {
118 return;
119 }
120 final SimpleBus bus = AmbientApplication.getInstance().getBus();
121 bus.getInvocator(IContentListener.class, true).coverLoaded(
122 (TrackMetadataBean) fetchInfo);
123 }
124
125 @Override
126 protected URL toURL(Object fetchInfo) throws IOException {
127 final TrackMetadataBean track = (TrackMetadataBean) fetchInfo;
128
129 final List<URL> covers = DownloadManager.getRemoteCoversList(track
130 .getArtist()
131 + " " + track.getAlbum());
132 if (covers.isEmpty())
133 return null;
134 if (Thread.currentThread().isInterrupted())
135 return null;
136 final URL cover = findSupported(covers);
137 return cover;
138 }
139
140 private URL findSupported(final List<URL> covers) {
141 for (final URL cover : covers) {
142 final String ext = "." + DownloadManager.getExtension(cover);
143 if (supportsExtension(ext))
144 return cover;
145 }
146 return null;
147 }
148
149 /***
150 * Fetches a cover for a track and sets it into given view. If the cover is
151 * not present in the cache and we are not offline the cover is fetched in a
152 * worker thread and {@link IContentListener#coverLoaded(TrackMetadataBean)}
153 * is emitted.
154 *
155 * @param track
156 * the track.
157 * @param view
158 * set the image to this view.
159 * @return loaded bitmap instance. The bitmap should be recycled when not
160 * needed.
161 */
162 public Bitmap setCover(final TrackMetadataBean track, final ImageView view) {
163 final File file = getCover(track);
164 if (file == null) {
165 view.setImageResource(R.drawable.cover);
166 return null;
167 } else {
168 final Bitmap bitmap = BitmapFactory.decodeFile(file
169 .getAbsolutePath());
170 view.setImageBitmap(bitmap);
171 return bitmap;
172 }
173 }
174
175 @Override
176 public void cleanup() {
177 removeObsoleteAlbums();
178 removeOldestCovers();
179 }
180
181 private synchronized void removeOldestCovers() {
182 if (!isFull())
183 return;
184 final List<File> files = new ArrayList<File>(theCache.values());
185
186 Collections.sort(files, new Comparator<File>() {
187 public int compare(File object1, File object2) {
188 if (object1.lastModified() < object2.lastModified()) {
189 return -1;
190 }
191 if (object1.lastModified() > object2.lastModified()) {
192 return 1;
193 }
194 return 0;
195 }
196 });
197 for (final File coverFile : files) {
198 removeFile(coverFile);
199 if (!isFull()) {
200 return;
201 }
202 }
203 }
204
205 private synchronized void removeObsoleteAlbums() {
206 if (!isFull()) {
207 return;
208 }
209 if (!libraryUpdated) {
210 return;
211 }
212 libraryUpdated = false;
213 final Set<String> obsoleteCovers = new HashSet<String>(theCache
214 .keySet());
215 removeKnownAlbums(obsoleteCovers, new LibraryCollection(AmbientApplication
216 .getInstance().getLibrary()));
217 removeKnownAlbums(obsoleteCovers, new MediaStoreCollection(
218 AmbientApplication.getInstance().getContentResolver()));
219
220 for (final String album : obsoleteCovers) {
221 final File coverFile = getCacheFile(album);
222 removeFile(coverFile);
223 }
224 }
225
226 private void removeKnownAlbums(Set<String> albums, ICollection c) {
227 try {
228 for (final CategoryItem item : c.getCategoryList(
229 CategoryEnum.Album, null, null, false)) {
230 albums.remove(item.name);
231 }
232 } catch (Exception e) {
233 throw new RuntimeException(e);
234 }
235 }
236
237 private volatile boolean libraryUpdated = false;
238
239 public void libraryUpdate(boolean updateStarted, boolean interrupted,
240 boolean userNotified) {
241 if (!updateStarted) {
242 libraryUpdated = true;
243 }
244 }
245 }