View Javadoc

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 			// these albums are to be downloaded. Maps to CategoryItem from the
135 			// "downloadFrom" collection. Key is the album name.
136 			final Map<String, CategoryItem> toSynchronize = new HashMap<String, CategoryItem>();
137 			// a subset of the "toSynchronize" map, containing albums which
138 			// already are partially present. Maps to CategoryItem from the
139 			// "reference" collection. Key is the album name.
140 			final Map<String, CategoryItem> missingTracks = new HashMap<String, CategoryItem>();
141 			// get the list of albums to synchronize
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 		// convert the "items" category list to a list of albums
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 }