2 * Copyright (C) 2014 Cyanogen, Inc.
4 package com.cyngn.eleven.utils;
6 import android.content.Context;
8 import com.cyngn.eleven.R;
9 import com.cyngn.eleven.model.Album;
10 import com.cyngn.eleven.model.Artist;
11 import com.cyngn.eleven.model.Song;
13 import java.util.List;
14 import java.util.TreeMap;
17 * This Utils class contains code that compares two different items and determines whether
18 * a section should be created
20 public class SectionCreatorUtils {
22 * Interface to compare two items and create labels
23 * @param <T> type of item to compare
25 public static interface IItemCompare<T> {
27 * Compares to items and returns a section divider T if there should
28 * be a section divider between first and second
29 * @param first the first element in the list. If null, it is checking to see
30 * if we need a divider at the beginning of the list
31 * @param second the second element in the list.
32 * @return String the expected separator label or null if none
34 public String createSectionSeparator(T first, T second);
37 * Returns the section label that corresponds to this item
38 * @param item the item
39 * @return the section label that this label falls under
41 public String createLabel(T item);
46 * A localized String comparison implementation of IItemCompare
47 * @param <T> the type of item to compare
49 public static abstract class LocalizedCompare<T> implements IItemCompare<T> {
51 public String createSectionSeparator(T first, T second) {
52 String secondLabel = createLabel(second);
53 // if we can't determine a good label then don't bother creating a section
54 if (secondLabel == null) {
58 if (first == null || !secondLabel.equals(createLabel(first))) {
66 public String createLabel(T item) {
67 return MusicUtils.getLocalizedBucketLetter(getString(item), trimName());
71 * @return true if we want to trim the name first - apparently artists don't trim
74 public boolean trimName() {
78 public abstract String getString(T item);
82 * A simple int comparison implementation of IItemCompare
83 * @param <T> the type of item to compare
85 public static abstract class IntCompare<T> implements IItemCompare<T> {
87 public String createSectionSeparator(T first, T second) {
88 if (first == null || getInt(first) != getInt(second)) {
89 return createLabel(second);
96 public String createLabel(T item) {
97 return String.valueOf(getInt(item));
100 public abstract int getInt(T item);
104 * A Bounded int comparison implementation of IntCompare
105 * Basically this will take ints and determine what bounds it falls into
106 * For example, 1-5 mintes, 5-10 minutes, 10+ minutes
107 * @param <T> the type of item to compare
109 public static abstract class BoundedIntCompare<T> extends IntCompare<T> {
110 protected Context mContext;
112 public BoundedIntCompare(Context context) {
116 protected abstract int getStringId(int value);
119 public String createSectionSeparator(T first, T second) {
120 int secondStringId = getStringId(getInt(second));
121 if (first == null || getStringId(getInt(first)) != secondStringId) {
122 return createLabel(secondStringId, second);
128 protected String createLabel(int stringId, T item) {
129 return mContext.getString(stringId);
133 public String createLabel(T item) {
134 return createLabel(getStringId(getInt(item)), item);
139 * This implements BoundedIntCompare and gives duration buckets
140 * @param <T> the type of item to compare
142 public static abstract class DurationCompare<T> extends BoundedIntCompare<T> {
143 private static final int SECONDS_PER_MINUTE = 60;
145 public DurationCompare(Context context) {
150 protected int getStringId(int value) {
152 return R.string.header_less_than_30s;
153 } else if (value < 1 * SECONDS_PER_MINUTE) {
154 return R.string.header_30_to_60_seconds;
155 } else if (value < 2 * SECONDS_PER_MINUTE) {
156 return R.string.header_1_to_2_minutes;
157 } else if (value < 3 * SECONDS_PER_MINUTE) {
158 return R.string.header_2_to_3_minutes;
159 } else if (value < 4 * SECONDS_PER_MINUTE) {
160 return R.string.header_3_to_4_minutes;
161 } else if (value < 5 * SECONDS_PER_MINUTE) {
162 return R.string.header_4_to_5_minutes;
163 } else if (value < 10 * SECONDS_PER_MINUTE) {
164 return R.string.header_5_to_10_minutes;
165 } else if (value < 30 * SECONDS_PER_MINUTE) {
166 return R.string.header_10_to_30_minutes;
167 } else if (value < 60 * SECONDS_PER_MINUTE) {
168 return R.string.header_30_to_60_minutes;
171 return R.string.header_greater_than_60_minutes;
176 * This implements BoundedIntCompare and gives number of songs buckets
177 * @param <T> the type of item to compare
179 public static abstract class NumberOfSongsCompare<T> extends BoundedIntCompare<T> {
180 public NumberOfSongsCompare(Context context) {
185 protected int getStringId(int value) {
187 return R.string.header_1_song;
188 } else if (value <= 4) {
189 return R.string.header_2_to_4_songs;
190 } else if (value <= 9) {
191 return R.string.header_5_to_9_songs;
194 return R.string.header_10_plus_songs;
199 * This implements BoundedIntCompare and gives number of albums buckets
200 * @param <T> the type of item to compare
202 public static abstract class NumberOfAlbumsCompare<T> extends BoundedIntCompare<T> {
203 public NumberOfAlbumsCompare(Context context) {
208 protected int getStringId(int value) {
210 return R.string.header_1_album;
211 } else if (value <= 4) {
212 return R.string.header_n_albums;
215 return R.string.header_5_plus_albums;
219 public String createSectionSeparator(T first, T second) {
220 boolean returnSeparator = false;
222 returnSeparator = true;
224 // create a separator if both album counts are different and they are
225 // not greater than 5 albums
226 int firstInt = getInt(first);
227 int secondInt = getInt(second);
228 if (firstInt != secondInt &&
229 !(firstInt >= 5 && secondInt >= 5)) {
230 returnSeparator = true;
234 if (returnSeparator) {
235 return createLabel(second);
242 protected String createLabel(int stringId, T item) {
243 if (stringId == R.string.header_n_albums) {
244 return mContext.getString(stringId, getInt(item));
247 return super.createLabel(stringId, item);
252 * This creates the sections give a list of items and the comparison algorithm
253 * @param list The list of items to analyze
254 * @param comparator The comparison function to use
255 * @param <T> the type of item to compare
256 * @return Creates a TreeMap of indices (if the headers were part of the list) to section labels
258 public static <T> TreeMap<Integer, String> createSections(final List<T> list,
259 final IItemCompare<T> comparator) {
260 if (list != null && list.size() > 0) {
261 TreeMap<Integer, String> sectionHeaders = new TreeMap<Integer, String>();
262 for (int i = 0; i < list.size(); i++) {
263 T first = (i == 0 ? null : list.get(i - 1));
264 T second = list.get(i);
266 String separator = comparator.createSectionSeparator(first, second);
267 if (separator != null) {
268 // add sectionHeaders.size() to store the indices of the combined list
269 sectionHeaders.put(sectionHeaders.size() + i, separator);
273 return sectionHeaders;
280 * Returns an artist comparison based on the current sort
281 * @param context Context for string generation
282 * @return the artist comparison method
284 public static IItemCompare<Artist> createArtistComparison(final Context context) {
285 IItemCompare<Artist> sectionCreator = null;
287 String sortOrder = PreferenceUtils.getInstance(context).getArtistSortOrder();
288 if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z)
289 || sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A)) {
290 sectionCreator = new SectionCreatorUtils.LocalizedCompare<Artist>() {
292 public String getString(Artist item) {
293 return item.mArtistName;
296 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS)) {
297 sectionCreator = new SectionCreatorUtils.NumberOfAlbumsCompare<Artist>(context) {
299 public int getInt(Artist item) {
300 return item.mAlbumNumber;
303 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS)) {
304 sectionCreator = new NumberOfSongsCompare<Artist>(context) {
306 public int getInt(Artist item) {
307 return item.mSongNumber;
312 return sectionCreator;
316 * Returns an album comparison based on the current sort
317 * @param context Context for string generation
318 * @return the album comparison method
320 public static IItemCompare<Album> createAlbumComparison(final Context context) {
321 IItemCompare<Album> sectionCreator = null;
323 String sortOrder = PreferenceUtils.getInstance(context).getAlbumSortOrder();
324 if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z)
325 || sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A)) {
326 sectionCreator = new LocalizedCompare<Album>() {
328 public String getString(Album item) {
329 return item.mAlbumName;
332 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
333 sectionCreator = new LocalizedCompare<Album>() {
335 public String getString(Album item) {
336 return item.mArtistName;
340 public boolean trimName() {
344 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS)) {
345 sectionCreator = new NumberOfSongsCompare<Album>(context) {
347 public int getInt(Album item) {
348 return item.mSongNumber;
351 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_YEAR)) {
352 sectionCreator = new IntCompare<Album>() {
353 private static final int INVALID_YEAR = -1;
356 public int getInt(Album item) {
357 // if we don't have a year, treat it as invalid
358 if (item.mYear == null) {
362 int year = Integer.valueOf(item.mYear);
364 // if the year is extremely low, treat it as invalid too
365 if (MusicUtils.isInvalidYear(year)) {
373 public String createLabel(Album item) {
374 if (MusicUtils.isInvalidYear(getInt(item))) {
375 return context.getString(R.string.header_unknown_year);
383 return sectionCreator;
387 * Returns an song comparison based on the current sort
388 * @param context Context for string generation
389 * @return the song comparison method
391 public static IItemCompare<Song> createSongComparison(final Context context) {
392 IItemCompare<Song> sectionCreator = null;
394 String sortOrder = PreferenceUtils.getInstance(context).getSongSortOrder();
396 // doesn't make sense to have headers for SONG_FILENAME
397 // so we will not return a sectionCreator for that one
398 if (sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z)
399 || sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A)) {
400 sectionCreator = new LocalizedCompare<Song>() {
402 public String getString(Song item) {
403 return item.mSongName;
406 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
407 sectionCreator = new LocalizedCompare<Song>() {
409 public String getString(Song item) {
410 return item.mAlbumName;
413 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
414 sectionCreator = new LocalizedCompare<Song>() {
416 public String getString(Song item) {
417 return item.mArtistName;
421 public boolean trimName() {
425 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_DURATION)) {
426 sectionCreator = new DurationCompare<Song>(context) {
428 public int getInt(Song item) {
429 return item.mDuration;
432 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_YEAR)) {
433 sectionCreator = new SectionCreatorUtils.IntCompare<Song>() {
435 public int getInt(Song item) {
440 public String createLabel(Song item) {
441 // I have seen tracks in my library where it would return 0 or 2
442 // so have this check to return a more friendly label in that case
443 if (MusicUtils.isInvalidYear(item.mYear)) {
444 return context.getString(R.string.header_unknown_year);
447 return super.createLabel(item);
452 return sectionCreator;