2 * Copyright (C) 2014 Cyanogen, Inc.
4 package com.cyngn.eleven.utils;
6 import android.content.Context;
8 import com.cyngn.eleven.Config;
9 import com.cyngn.eleven.R;
10 import com.cyngn.eleven.model.Album;
11 import com.cyngn.eleven.model.Artist;
12 import com.cyngn.eleven.model.SearchResult;
13 import com.cyngn.eleven.model.Song;
15 import java.util.List;
16 import java.util.TreeMap;
19 * This Utils class contains code that compares two different items and determines whether
20 * a section should be created
22 public class SectionCreatorUtils {
23 public enum SectionType {
28 public static class Section {
29 public SectionType mType;
30 public String mIdentifier;
32 public Section(final SectionType type, final String identifier) {
34 mIdentifier = identifier;
39 * Interface to compare two items and create labels
40 * @param <T> type of item to compare
42 public static class IItemCompare<T> {
44 * Compares to items and returns a section divider T if there should
45 * be a section divider between first and second
46 * @param first the first element in the list. If null, it is checking to see
47 * if we need a divider at the beginning of the list
48 * @param second the second element in the list.
49 * @param items the source list of items that we are creating headers from
50 * @param firstIndex index of the first item we are looking at
51 * @return String the expected separator label or null if none
53 public String createSectionHeader(T first, T second, List<T> items, int firstIndex) {
54 return createSectionHeader(first, second);
57 public String createSectionHeader(T first, T second) {
62 * Compares to items and returns a section divider T if there should
63 * be a section divider between first and second
64 * @param first the first element in the list.
65 * @param second the second element in the list. If null, it is checking to see if we need
66 * a divider at the end of the list
67 * @param items the source list of items that we are creating footers from
68 * @param firstIndex index of the first item we are looking at
69 * @return String the expected separator label or null if none
71 public String createSectionFooter(T first, T second, List<T> items, int firstIndex) {
72 return createSectionFooter(first, second);
75 public String createSectionFooter(T first, T second) {
80 * Returns the section label that corresponds to this item
81 * @param item the item
82 * @return the section label that this label falls under
84 public String createHeaderLabel(T item) {
89 * Returns the section label that corresponds to this item
90 * @param item the item
91 * @return the section label that this label falls under
93 public String createFooterLabel(T item) {
99 * A localized String comparison implementation of IItemCompare
100 * @param <T> the type of item to compare
102 public static abstract class LocalizedCompare<T> extends IItemCompare<T> {
104 public String createSectionHeader(T first, T second) {
105 String secondLabel = createHeaderLabel(second);
106 // if we can't determine a good label then don't bother creating a section
107 if (secondLabel == null) {
111 if (first == null || !secondLabel.equals(createHeaderLabel(first))) {
119 public String createHeaderLabel(T item) {
120 return MusicUtils.getLocalizedBucketLetter(getString(item), trimName());
124 * @return true if we want to trim the name first - apparently artists don't trim
125 * but albums/songs do
127 public boolean trimName() {
131 public abstract String getString(T item);
135 * A simple int comparison implementation of IItemCompare
136 * @param <T> the type of item to compare
138 public static abstract class IntCompare<T> extends IItemCompare<T> {
140 public String createSectionHeader(T first, T second) {
141 if (first == null || getInt(first) != getInt(second)) {
142 return createHeaderLabel(second);
149 public String createHeaderLabel(T item) {
150 return String.valueOf(getInt(item));
153 public abstract int getInt(T item);
157 * A Bounded int comparison implementation of IntCompare
158 * Basically this will take ints and determine what bounds it falls into
159 * For example, 1-5 mintes, 5-10 minutes, 10+ minutes
160 * @param <T> the type of item to compare
162 public static abstract class BoundedIntCompare<T> extends IntCompare<T> {
163 protected Context mContext;
165 public BoundedIntCompare(Context context) {
169 protected abstract int getStringId(int value);
172 public String createSectionHeader(T first, T second) {
173 int secondStringId = getStringId(getInt(second));
174 if (first == null || getStringId(getInt(first)) != secondStringId) {
175 return createLabel(secondStringId, second);
181 protected String createLabel(int stringId, T item) {
182 return mContext.getString(stringId);
186 public String createHeaderLabel(T item) {
187 return createLabel(getStringId(getInt(item)), item);
192 * This implements BoundedIntCompare and gives duration buckets
193 * @param <T> the type of item to compare
195 public static abstract class DurationCompare<T> extends BoundedIntCompare<T> {
196 private static final int SECONDS_PER_MINUTE = 60;
198 public DurationCompare(Context context) {
203 protected int getStringId(int value) {
205 return R.string.header_less_than_30s;
206 } else if (value < 1 * SECONDS_PER_MINUTE) {
207 return R.string.header_30_to_60_seconds;
208 } else if (value < 2 * SECONDS_PER_MINUTE) {
209 return R.string.header_1_to_2_minutes;
210 } else if (value < 3 * SECONDS_PER_MINUTE) {
211 return R.string.header_2_to_3_minutes;
212 } else if (value < 4 * SECONDS_PER_MINUTE) {
213 return R.string.header_3_to_4_minutes;
214 } else if (value < 5 * SECONDS_PER_MINUTE) {
215 return R.string.header_4_to_5_minutes;
216 } else if (value < 10 * SECONDS_PER_MINUTE) {
217 return R.string.header_5_to_10_minutes;
218 } else if (value < 30 * SECONDS_PER_MINUTE) {
219 return R.string.header_10_to_30_minutes;
220 } else if (value < 60 * SECONDS_PER_MINUTE) {
221 return R.string.header_30_to_60_minutes;
224 return R.string.header_greater_than_60_minutes;
229 * This implements BoundedIntCompare and gives number of songs buckets
230 * @param <T> the type of item to compare
232 public static abstract class NumberOfSongsCompare<T> extends BoundedIntCompare<T> {
233 public NumberOfSongsCompare(Context context) {
238 protected int getStringId(int value) {
240 return R.string.header_1_song;
241 } else if (value <= 4) {
242 return R.string.header_2_to_4_songs;
243 } else if (value <= 9) {
244 return R.string.header_5_to_9_songs;
247 return R.string.header_10_plus_songs;
252 * This implements BoundedIntCompare and gives number of albums buckets
253 * @param <T> the type of item to compare
255 public static abstract class NumberOfAlbumsCompare<T> extends BoundedIntCompare<T> {
256 public NumberOfAlbumsCompare(Context context) {
261 protected int getStringId(int value) {
263 return R.string.header_1_album;
264 } else if (value <= 4) {
265 return R.string.header_n_albums;
268 return R.string.header_5_plus_albums;
272 public String createSectionHeader(T first, T second) {
273 boolean returnSeparator = false;
275 returnSeparator = true;
277 // create a separator if both album counts are different and they are
278 // not greater than 5 albums
279 int firstInt = getInt(first);
280 int secondInt = getInt(second);
281 if (firstInt != secondInt &&
282 !(firstInt >= 5 && secondInt >= 5)) {
283 returnSeparator = true;
287 if (returnSeparator) {
288 return createHeaderLabel(second);
295 protected String createLabel(int stringId, T item) {
296 if (stringId == R.string.header_n_albums) {
297 return mContext.getString(stringId, getInt(item));
300 return super.createLabel(stringId, item);
305 * This creates the sections give a list of items and the comparison algorithm
306 * @param list The list of items to analyze
307 * @param comparator The comparison function to use
308 * @param <T> the type of item to compare
309 * @return Creates a TreeMap of indices (if the headers were part of the list) to section labels
311 public static <T> TreeMap<Integer, Section> createSections(final List<T> list,
312 final IItemCompare<T> comparator) {
313 if (list != null && list.size() > 0) {
314 TreeMap<Integer, Section> sections = new TreeMap<Integer, Section>();
315 for (int i = 0; i < list.size() + 1; i++) {
316 T first = (i == 0 ? null : list.get(i - 1));
317 T second = (i == list.size() ? null : list.get(i));
319 // create the footer first because if we need both it should be footer,header,item
320 // not header,footer,item
322 String footer = comparator.createSectionFooter(first, second, list, i - 1);
323 if (footer != null) {
324 // add sectionHeaders.size() to store the indices of the combined list
325 sections.put(sections.size() + i, new Section(SectionType.Footer, footer));
329 if (second != null) {
330 String header = comparator.createSectionHeader(first, second, list, i - 1);
331 if (header != null) {
332 // add sectionHeaders.size() to store the indices of the combined list
333 sections.put(sections.size() + i, new Section(SectionType.Header, header));
345 * Returns an artist comparison based on the current sort
346 * @param context Context for string generation
347 * @return the artist comparison method
349 public static IItemCompare<Artist> createArtistComparison(final Context context) {
350 IItemCompare<Artist> sectionCreator = null;
352 String sortOrder = PreferenceUtils.getInstance(context).getArtistSortOrder();
353 if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z)
354 || sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A)) {
355 sectionCreator = new SectionCreatorUtils.LocalizedCompare<Artist>() {
357 public String getString(Artist item) {
358 return item.mArtistName;
361 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS)) {
362 sectionCreator = new SectionCreatorUtils.NumberOfAlbumsCompare<Artist>(context) {
364 public int getInt(Artist item) {
365 return item.mAlbumNumber;
368 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS)) {
369 sectionCreator = new NumberOfSongsCompare<Artist>(context) {
371 public int getInt(Artist item) {
372 return item.mSongNumber;
377 return sectionCreator;
381 * Returns an album comparison based on the current sort
382 * @param context Context for string generation
383 * @return the album comparison method
385 public static IItemCompare<Album> createAlbumComparison(final Context context) {
386 IItemCompare<Album> sectionCreator = null;
388 String sortOrder = PreferenceUtils.getInstance(context).getAlbumSortOrder();
389 if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z)
390 || sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A)) {
391 sectionCreator = new LocalizedCompare<Album>() {
393 public String getString(Album item) {
394 return item.mAlbumName;
397 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
398 sectionCreator = new LocalizedCompare<Album>() {
400 public String getString(Album item) {
401 return item.mArtistName;
405 public boolean trimName() {
409 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS)) {
410 sectionCreator = new NumberOfSongsCompare<Album>(context) {
412 public int getInt(Album item) {
413 return item.mSongNumber;
416 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_YEAR)) {
417 sectionCreator = new IntCompare<Album>() {
418 private static final int INVALID_YEAR = -1;
421 public int getInt(Album item) {
422 // if we don't have a year, treat it as invalid
423 if (item.mYear == null) {
427 int year = Integer.valueOf(item.mYear);
429 // if the year is extremely low, treat it as invalid too
430 if (MusicUtils.isInvalidYear(year)) {
438 public String createHeaderLabel(Album item) {
439 if (MusicUtils.isInvalidYear(getInt(item))) {
440 return context.getString(R.string.header_unknown_year);
448 return sectionCreator;
452 * Returns an song comparison based on the current sort
453 * @param context Context for string generation
454 * @return the song comparison method
456 public static IItemCompare<Song> createSongComparison(final Context context) {
457 IItemCompare<Song> sectionCreator = null;
459 String sortOrder = PreferenceUtils.getInstance(context).getSongSortOrder();
461 // doesn't make sense to have headers for SONG_FILENAME
462 // so we will not return a sectionCreator for that one
463 if (sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z)
464 || sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A)) {
465 sectionCreator = new LocalizedCompare<Song>() {
467 public String getString(Song item) {
468 return item.mSongName;
471 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
472 sectionCreator = new LocalizedCompare<Song>() {
474 public String getString(Song item) {
475 return item.mAlbumName;
478 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
479 sectionCreator = new LocalizedCompare<Song>() {
481 public String getString(Song item) {
482 return item.mArtistName;
486 public boolean trimName() {
490 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_DURATION)) {
491 sectionCreator = new DurationCompare<Song>(context) {
493 public int getInt(Song item) {
494 return item.mDuration;
497 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_YEAR)) {
498 sectionCreator = new SectionCreatorUtils.IntCompare<Song>() {
500 public int getInt(Song item) {
505 public String createHeaderLabel(Song item) {
506 // I have seen tracks in my library where it would return 0 or 2
507 // so have this check to return a more friendly label in that case
508 if (MusicUtils.isInvalidYear(item.mYear)) {
509 return context.getString(R.string.header_unknown_year);
512 return super.createHeaderLabel(item);
517 return sectionCreator;
521 * Returns an song comparison based on the current sort
522 * @param context Context for string generation
523 * @return the song comparison method
525 public static IItemCompare<SearchResult> createSearchResultComparison(final Context context) {
526 return new IItemCompare<SearchResult>() {
529 public String createSectionHeader(SearchResult first, SearchResult second) {
530 if (first == null || first.mType != second.mType) {
531 return createHeaderLabel(second);
538 public String createHeaderLabel(SearchResult item) {
539 switch (item.mType) {
541 return context.getString(R.string.page_artists);
543 return context.getString(R.string.page_albums);
545 return context.getString(R.string.page_songs);
547 return context.getString(R.string.page_playlists);
554 public String createSectionFooter(SearchResult first, SearchResult second,
555 List<SearchResult> items, int firstIndex) {
556 if (second == null ||
557 (first != null && first.mType != second.mType)) {
558 // if we don't have SEARCH_NUM_RESULTS_TO_GET # of the same type of items
559 // then we don't have enough to show the footer. For example, if we show 5
560 // items but only the last 2 items are artists, that means we only have 2
561 // so there is no point in showing the "Show All" footer
562 // We start from 1 because we don't need to count
563 // the first item itself
564 for (int i = 1; i < Config.SEARCH_NUM_RESULTS_TO_GET; i++) {
565 if (firstIndex - i < 0 || items.get(firstIndex - i).mType != first.mType) {
570 return createFooterLabel(first);
577 public String createFooterLabel(SearchResult item) {
578 switch (item.mType) {
580 return context.getString(R.string.footer_search_artists);
582 return context.getString(R.string.footer_search_albums);
584 return context.getString(R.string.footer_search_songs);
586 return context.getString(R.string.footer_search_playlists);