1 package sk.baka.ambient.collection.sync;
2
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.HashMap;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.concurrent.Callable;
10
11 import android.content.Context;
12 import android.media.MediaScannerConnection;
13
14 import sk.baka.ambient.AmbientApplication;
15 import sk.baka.ambient.IBackgroundTask;
16 import sk.baka.ambient.R;
17 import sk.baka.ambient.collection.CategoryEnum;
18 import sk.baka.ambient.collection.CategoryItem;
19 import sk.baka.ambient.collection.CollectionException;
20 import sk.baka.ambient.collection.ICollection;
21 import sk.baka.ambient.collection.TrackMetadataBean;
22 import sk.baka.ambient.commons.IOUtils;
23 import sk.baka.ambient.commons.MiscUtils;
24 import sk.baka.ambient.commons.TagFormatter;
25
26 /***
27 * Downloads tracks from given collection which are missing in the reference
28 * collection. All work is done in the {@link #call()} method.
29 *
30 * @author mvy
31 */
32 public class CollectionSynchronizer implements Callable<Void> {
33 /***
34 * Download excess tracks from this collection.
35 */
36 private final ICollection downloadFrom;
37 /***
38 * This is the reference collection, i.e. tracks which are not to be
39 * synchronized.
40 */
41 private final ICollection reference;
42 /***
43 * This path is automatically prepended to paths produced by the
44 * <code>pathFormatter</code> formatter.
45 */
46 private final String rootPath;
47 private final IBackgroundTask task;
48 private final SyncUtils utils;
49 private final MediaScannerConnection msc;
50
51 /***
52 * Creates new synchronizer.
53 *
54 * @param context
55 * owner's context.
56 * @param reference
57 * This is the reference collection, i.e. tracks which are not to
58 * be synchronized.
59 * @param downloadFrom
60 * Download excess tracks from this collection.
61 * @param pathFormatter
62 * produces a relative path where the music file should be
63 * stored. Must produce valid paths! Paths are created
64 * automatically.
65 * @param rootPath
66 * this path is automatically prepended to paths produced by the
67 * <code>pathFormatter</code> formatter.
68 * @param task
69 * used for progress reporting, may be <code>null</code>.
70 */
71 public CollectionSynchronizer(final Context context,
72 final ICollection reference, final ICollection downloadFrom,
73 final TagFormatter pathFormatter, final String rootPath,
74 final IBackgroundTask task) {
75 this.reference = reference;
76 this.downloadFrom = downloadFrom;
77 this.task = task;
78 this.rootPath = IOUtils.removeTrailingSlash(rootPath);
79 this.msc = new MediaScannerConnection(context, null);
80 this.utils = new SyncUtils(reference, downloadFrom, pathFormatter, msc);
81 }
82
83 private Collection<? extends TrackMetadataBean> tracksToDownload = null;
84
85 /***
86 * Sets the synchronizer to download these tracks, instead of performing a
87 * full synchronization.
88 *
89 * @param tracks
90 * the tracks to download.
91 */
92 public void setTracksToDownload(
93 final Collection<? extends TrackMetadataBean> tracks) {
94 if (tracksToDownload.isEmpty()) {
95 throw new IllegalArgumentException("items must not be empty");
96 }
97 tracksToDownload = tracks;
98 items = null;
99 }
100
101 private Collection<? extends CategoryItem> items;
102
103 /***
104 * Sets the synchronizer to download tracks from these categories, instead
105 * of performing a full synchronization.
106 *
107 * @param items
108 * the items to download.
109 */
110 public void setCategoriesToDownload(
111 final Collection<? extends CategoryItem> items) {
112 if (items.isEmpty()) {
113 throw new IllegalArgumentException("items must not be empty");
114 }
115 this.items = items;
116 tracksToDownload = null;
117 }
118
119 /***
120 * Configures the synchronizer to download entire collection (the default).
121 */
122 public void downloadEntireCollection() {
123 items = null;
124 tracksToDownload = null;
125 }
126
127 public Void call() throws Exception {
128 msc.connect();
129 try {
130 if (tracksToDownload != null) {
131 downloadTracks(tracksToDownload);
132 return null;
133 }
134
135
136 final Map<String, CategoryItem> toSynchronize = new HashMap<String, CategoryItem>();
137
138
139
140 final Map<String, CategoryItem> missingTracks = new HashMap<String, CategoryItem>();
141
142 final Collection<? extends CategoryItem> albums = getAlbums();
143 fetchAlbumsToSynchronize(albums, toSynchronize, missingTracks);
144 synchronize(toSynchronize, missingTracks);
145 return null;
146 } finally {
147 msc.disconnect();
148 }
149 }
150
151 private Collection<? extends CategoryItem> getAlbums()
152 throws CollectionException, InterruptedException {
153 if (items == null) {
154 return downloadFrom.getCategoryList(CategoryEnum.Album, null, null,
155 false);
156 }
157
158 final CategoryEnum itemsType = getCategoryType(items);
159 if (itemsType == CategoryEnum.Album) {
160 return items;
161 }
162 final List<CategoryItem> result = new ArrayList<CategoryItem>();
163 for (final CategoryItem item : items) {
164 final List<CategoryItem> albums = downloadFrom.getCategoryList(
165 CategoryEnum.Album, item, null, false);
166 result.addAll(albums);
167 }
168 return result;
169 }
170
171 private CategoryEnum getCategoryType(
172 final Collection<? extends CategoryItem> items) {
173 return items.iterator().next().category;
174 }
175
176 private void synchronize(final Map<String, CategoryItem> toSynchronize,
177 final Map<String, CategoryItem> missingTracks) throws Exception {
178 int synchronizedAlbums = 0;
179 final int maxProgress = 1 + toSynchronize.size();
180 for (final Map.Entry<String, CategoryItem> album : toSynchronize
181 .entrySet()) {
182 if (task != null) {
183 final String progressName = MiscUtils.format(
184 R.string.downloadingAlbum, album.getKey());
185 task.backgroundTask(0, progressName, ++synchronizedAlbums,
186 maxProgress);
187 }
188 final boolean downloadPartly = missingTracks.containsKey(album
189 .getKey());
190 final List<TrackMetadataBean> tracksToDownload = downloadFrom
191 .getTracks(album.getValue());
192 if (downloadPartly) {
193 final List<TrackMetadataBean> localTracks = reference
194 .getTracks(missingTracks.get(album.getKey()));
195 SyncUtils.removeLocalTracks(tracksToDownload, localTracks);
196 }
197 downloadTracks(tracksToDownload);
198 }
199 }
200
201 private void downloadTracks(
202 Collection<? extends TrackMetadataBean> tracksToDownload)
203 throws Exception {
204 for (final TrackMetadataBean track : tracksToDownload) {
205 try {
206 utils.downloadTrack(rootPath, track);
207 } catch (final Exception ex) {
208 if (Thread.currentThread().isInterrupted()) {
209 throw ex;
210 }
211 final String errorMsg = MiscUtils.format(
212 R.string.failedToDownload, track.getDisplayableName(),
213 ex.getMessage());
214 AmbientApplication.getInstance().error(
215 CollectionSynchronizer.class, true, errorMsg, ex);
216 }
217 }
218 }
219
220 /***
221 * Fetches albums to be synchronized.
222 *
223 * @param toSynchronize
224 * these albums are to be downloaded. Maps to
225 * {@link CategoryItem} from {@link #downloadFrom}. Key is the
226 * album name.
227 * @param missingTracks
228 * a subset of <code>toSynchronize</code>, contains albums which
229 * already are partially present. Maps to {@link CategoryItem}
230 * from {@link #reference}. Key is the album name.
231 * @throws InterruptedException
232 * @throws CollectionException
233 */
234 private void fetchAlbumsToSynchronize(
235 final Collection<? extends CategoryItem> remoteAlbumsToDownload,
236 Map<String, CategoryItem> toSynchronize,
237 Map<String, CategoryItem> missingTracks)
238 throws CollectionException, InterruptedException {
239 final Map<String, CategoryItem> localAlbums = getNames(reference
240 .getCategoryList(CategoryEnum.Album, null, null, false));
241 final Map<String, CategoryItem> remoteAlbums = getNames(remoteAlbumsToDownload);
242 missingTracks.putAll(localAlbums);
243 missingTracks.keySet().retainAll(remoteAlbums.keySet());
244 for (final Iterator<Map.Entry<String, CategoryItem>> entryIterator = missingTracks
245 .entrySet().iterator(); entryIterator.hasNext();) {
246 final Map.Entry<String, CategoryItem> entry = entryIterator.next();
247 int localTrackCount = entry.getValue().songs;
248 if (localTrackCount < 0) {
249 localTrackCount = reference.getTracks(entry.getValue()).size();
250 }
251 final CategoryItem remoteAlbum = remoteAlbums.get(entry.getKey());
252 int remoteTrackCount = remoteAlbum.songs;
253 if (remoteTrackCount < 0) {
254 remoteTrackCount = downloadFrom.getTracks(remoteAlbum).size();
255 }
256 if (localTrackCount >= remoteTrackCount) {
257 entryIterator.remove();
258 }
259 }
260 toSynchronize.putAll(remoteAlbums);
261 localAlbums.keySet().removeAll(missingTracks.keySet());
262 toSynchronize.keySet().removeAll(localAlbums.keySet());
263 }
264
265 private Map<String, CategoryItem> getNames(
266 final Collection<? extends CategoryItem> items) {
267 final Map<String, CategoryItem> result = new HashMap<String, CategoryItem>(
268 items.size());
269 for (final CategoryItem item : items) {
270 if (item.name != null) {
271 result.put(item.name, item);
272 }
273 }
274 return result;
275 }
276 }