2 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.lineageos.eleven.utils;
18 import android.content.Context;
19 import android.text.TextUtils;
21 import org.lineageos.eleven.Config;
22 import org.lineageos.eleven.R;
23 import org.lineageos.eleven.model.Album;
24 import org.lineageos.eleven.model.Artist;
25 import org.lineageos.eleven.model.SearchResult;
26 import org.lineageos.eleven.model.Song;
28 import java.util.List;
29 import java.util.TreeMap;
32 * This Utils class contains code that compares two different items and determines whether
33 * a section should be created
35 public class SectionCreatorUtils {
36 public enum SectionType {
41 public static class Section {
42 public SectionType mType;
43 public String mIdentifier;
45 public Section(final SectionType type, final String identifier) {
47 mIdentifier = identifier;
52 * Interface to compare two items and create labels
53 * @param <T> type of item to compare
55 public static class IItemCompare<T> {
57 * Compares to items and returns a section divider T if there should
58 * be a section divider between first and second
59 * @param first the first element in the list. If null, it is checking to see
60 * if we need a divider at the beginning of the list
61 * @param second the second element in the list.
62 * @param items the source list of items that we are creating headers from
63 * @param firstIndex index of the first item we are looking at
64 * @return String the expected separator label or null if none
66 public String createSectionHeader(T first, T second, List<T> items, int firstIndex) {
67 return createSectionHeader(first, second);
70 public String createSectionHeader(T first, T second) {
75 * Compares to items and returns a section divider T if there should
76 * be a section divider between first and second
77 * @param first the first element in the list.
78 * @param second the second element in the list. If null, it is checking to see if we need
79 * a divider at the end of the list
80 * @param items the source list of items that we are creating footers from
81 * @param firstIndex index of the first item we are looking at
82 * @return String the expected separator label or null if none
84 public String createSectionFooter(T first, T second, List<T> items, int firstIndex) {
85 return createSectionFooter(first, second);
88 public String createSectionFooter(T first, T second) {
93 * Returns the section label that corresponds to this item
94 * @param item the item
95 * @return the section label that this label falls under
97 public String createHeaderLabel(T item) {
102 * Returns the section label that corresponds to this item
103 * @param item the item
104 * @return the section label that this label falls under
106 public String createFooterLabel(T item) {
110 // partial sectioning helper functions
112 public boolean shouldStopSectionCreation() {
118 * A localized String comparison implementation of IItemCompare
119 * @param <T> the type of item to compare
121 public static abstract class LocalizedCompare<T> extends IItemCompare<T> {
122 protected Context mContext;
123 private boolean mStopSectionCreation;
125 public LocalizedCompare(Context context) {
127 mStopSectionCreation = false;
131 public String createSectionHeader(T first, T second) {
132 String secondLabel = createHeaderLabel(second);
133 // if we can't determine a good label then don't bother creating a section
134 if (secondLabel == null) {
135 // stop section creation as the items further down the list
136 mStopSectionCreation = true;
140 if (first == null || !secondLabel.equals(createHeaderLabel(first))) {
148 public String createHeaderLabel(T item) {
149 final String label = MusicUtils.getLocalizedBucketLetter(getString(item));
150 return createHeaderLabel(label);
153 protected String createHeaderLabel(final String label) {
154 if (TextUtils.isEmpty(label)) {
155 return mContext.getString(R.string.header_other);
160 public abstract String getString(T item);
163 public boolean shouldStopSectionCreation() {
164 return mStopSectionCreation;
169 * A simple int comparison implementation of IItemCompare
170 * @param <T> the type of item to compare
172 public static abstract class IntCompare<T> extends IItemCompare<T> {
174 public String createSectionHeader(T first, T second) {
175 if (first == null || getInt(first) != getInt(second)) {
176 return createHeaderLabel(second);
183 public String createHeaderLabel(T item) {
184 return String.valueOf(getInt(item));
187 public abstract int getInt(T item);
191 * A Bounded int comparison implementation of IntCompare
192 * Basically this will take ints and determine what bounds it falls into
193 * For example, 1-5 mintes, 5-10 minutes, 10+ minutes
194 * @param <T> the type of item to compare
196 public static abstract class BoundedIntCompare<T> extends IntCompare<T> {
197 protected Context mContext;
199 public BoundedIntCompare(Context context) {
203 protected abstract int getStringId(int value);
206 public String createSectionHeader(T first, T second) {
207 int secondStringId = getStringId(getInt(second));
208 if (first == null || getStringId(getInt(first)) != secondStringId) {
209 return createLabel(secondStringId, second);
215 protected String createLabel(int stringId, T item) {
216 return mContext.getString(stringId);
220 public String createHeaderLabel(T item) {
221 return createLabel(getStringId(getInt(item)), item);
226 * This implements BoundedIntCompare and gives duration buckets
227 * @param <T> the type of item to compare
229 public static abstract class DurationCompare<T> extends BoundedIntCompare<T> {
230 private static final int SECONDS_PER_MINUTE = 60;
232 public DurationCompare(Context context) {
237 protected int getStringId(int value) {
239 return R.string.header_less_than_30s;
240 } else if (value < 1 * SECONDS_PER_MINUTE) {
241 return R.string.header_30_to_60_seconds;
242 } else if (value < 2 * SECONDS_PER_MINUTE) {
243 return R.string.header_1_to_2_minutes;
244 } else if (value < 3 * SECONDS_PER_MINUTE) {
245 return R.string.header_2_to_3_minutes;
246 } else if (value < 4 * SECONDS_PER_MINUTE) {
247 return R.string.header_3_to_4_minutes;
248 } else if (value < 5 * SECONDS_PER_MINUTE) {
249 return R.string.header_4_to_5_minutes;
250 } else if (value < 10 * SECONDS_PER_MINUTE) {
251 return R.string.header_5_to_10_minutes;
252 } else if (value < 30 * SECONDS_PER_MINUTE) {
253 return R.string.header_10_to_30_minutes;
254 } else if (value < 60 * SECONDS_PER_MINUTE) {
255 return R.string.header_30_to_60_minutes;
258 return R.string.header_greater_than_60_minutes;
263 * This implements BoundedIntCompare and gives number of songs buckets
264 * @param <T> the type of item to compare
266 public static abstract class NumberOfSongsCompare<T> extends BoundedIntCompare<T> {
267 public NumberOfSongsCompare(Context context) {
272 protected int getStringId(int value) {
274 return R.string.header_1_song;
275 } else if (value <= 4) {
276 return R.string.header_2_to_4_songs;
277 } else if (value <= 9) {
278 return R.string.header_5_to_9_songs;
281 return R.string.header_10_plus_songs;
286 * This implements BoundedIntCompare and gives number of albums buckets
287 * @param <T> the type of item to compare
289 public static abstract class NumberOfAlbumsCompare<T> extends BoundedIntCompare<T> {
290 public NumberOfAlbumsCompare(Context context) {
295 protected int getStringId(int value) {
297 return R.plurals.Nalbums;
300 return R.string.header_5_plus_albums;
304 public String createSectionHeader(T first, T second) {
305 boolean returnSeparator = false;
307 returnSeparator = true;
309 // create a separator if both album counts are different and they are
310 // not greater than 5 albums
311 int firstInt = getInt(first);
312 int secondInt = getInt(second);
313 if (firstInt != secondInt &&
314 !(firstInt >= 5 && secondInt >= 5)) {
315 returnSeparator = true;
319 if (returnSeparator) {
320 return createHeaderLabel(second);
327 protected String createLabel(int stringId, T item) {
328 if (stringId == R.plurals.Nalbums) {
329 final int numItems = getInt(item);
330 return mContext.getResources().getQuantityString(stringId, numItems, numItems);
333 return super.createLabel(stringId, item);
338 * This creates the sections given a list of items and the comparison algorithm
339 * @param list The list of items to analyze
340 * @param comparator The comparison function to use
341 * @param <T> the type of item to compare
342 * @return Creates a TreeMap of indices (if the headers were part of the list) to section labels
344 public static <T> TreeMap<Integer, Section> createSections(final List<T> list,
345 final IItemCompare<T> comparator) {
346 if (list != null && list.size() > 0) {
347 TreeMap<Integer, Section> sections = new TreeMap<Integer, Section>();
348 for (int i = 0; i < list.size() + 1; i++) {
349 T first = (i == 0 ? null : list.get(i - 1));
350 T second = (i == list.size() ? null : list.get(i));
352 // create the footer first because if we need both it should be footer,header,item
353 // not header,footer,item
355 String footer = comparator.createSectionFooter(first, second, list, i - 1);
356 if (footer != null) {
357 // add sectionHeaders.size() to store the indices of the combined list
358 sections.put(sections.size() + i, new Section(SectionType.Footer, footer));
362 if (second != null) {
363 String header = comparator.createSectionHeader(first, second, list, i - 1);
364 if (header != null) {
365 // add sectionHeaders.size() to store the indices of the combined list
366 sections.put(sections.size() + i, new Section(SectionType.Header, header));
367 // stop section creation
368 if (comparator.shouldStopSectionCreation()) {
382 * Returns an artist comparison based on the current sort
383 * @param context Context for string generation
384 * @return the artist comparison method
386 public static IItemCompare<Artist> createArtistComparison(final Context context) {
387 IItemCompare<Artist> sectionCreator = null;
389 String sortOrder = PreferenceUtils.getInstance(context).getArtistSortOrder();
390 if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z)
391 || sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A)) {
392 sectionCreator = new SectionCreatorUtils.LocalizedCompare<Artist>(context) {
394 public String getString(Artist item) {
395 return item.mArtistName;
399 public String createHeaderLabel(Artist item) {
400 if (item.mBucketLabel != null) {
401 return super.createHeaderLabel(item.mBucketLabel);
404 return super.createHeaderLabel(item);
407 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS)) {
408 sectionCreator = new SectionCreatorUtils.NumberOfAlbumsCompare<Artist>(context) {
410 public int getInt(Artist item) {
411 return item.mAlbumNumber;
414 } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS)) {
415 sectionCreator = new NumberOfSongsCompare<Artist>(context) {
417 public int getInt(Artist item) {
418 return item.mSongNumber;
423 return sectionCreator;
427 * Returns an album comparison based on the current sort
428 * @param context Context for string generation
429 * @return the album comparison method
431 public static IItemCompare<Album> createAlbumComparison(final Context context) {
432 IItemCompare<Album> sectionCreator = null;
434 String sortOrder = PreferenceUtils.getInstance(context).getAlbumSortOrder();
435 if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z)
436 || sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A)) {
437 sectionCreator = new LocalizedCompare<Album>(context) {
439 public String getString(Album item) {
440 return item.mAlbumName;
444 public String createHeaderLabel(Album item) {
445 if (item.mBucketLabel != null) {
446 return super.createHeaderLabel(item.mBucketLabel);
449 return super.createHeaderLabel(item);
452 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
453 sectionCreator = new LocalizedCompare<Album>(context) {
455 public String getString(Album item) {
456 return item.mArtistName;
460 public String createHeaderLabel(Album item) {
461 if (item.mBucketLabel != null) {
462 return super.createHeaderLabel(item.mBucketLabel);
465 return super.createHeaderLabel(item);
468 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS)) {
469 sectionCreator = new NumberOfSongsCompare<Album>(context) {
471 public int getInt(Album item) {
472 return item.mSongNumber;
475 } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_YEAR)) {
476 sectionCreator = new IntCompare<Album>() {
477 private static final int INVALID_YEAR = -1;
480 public int getInt(Album item) {
481 // if we don't have a year, treat it as invalid
482 if (item.mYear == null) {
486 int year = Integer.valueOf(item.mYear);
488 // if the year is extremely low, treat it as invalid too
489 if (MusicUtils.isInvalidYear(year)) {
497 public String createHeaderLabel(Album item) {
498 if (MusicUtils.isInvalidYear(getInt(item))) {
499 return context.getString(R.string.header_unknown_year);
507 return sectionCreator;
511 * Returns an song comparison based on the current sort
512 * @param context Context for string generation
513 * @return the song comparison method
515 public static IItemCompare<Song> createSongComparison(final Context context) {
516 IItemCompare<Song> sectionCreator = null;
518 String sortOrder = PreferenceUtils.getInstance(context).getSongSortOrder();
520 // doesn't make sense to have headers for SONG_FILENAME
521 // so we will not return a sectionCreator for that one
522 if (sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z)
523 || sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A)) {
524 sectionCreator = new LocalizedCompare<Song>(context) {
526 public String getString(Song item) {
527 return item.mSongName;
531 public String createHeaderLabel(Song item) {
532 if (item.mBucketLabel != null) {
533 return super.createHeaderLabel(item.mBucketLabel);
536 return super.createHeaderLabel(item);
539 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
540 sectionCreator = new LocalizedCompare<Song>(context) {
542 public String getString(Song item) {
543 return item.mAlbumName;
547 public String createHeaderLabel(Song item) {
548 if (item.mBucketLabel != null) {
549 return super.createHeaderLabel(item.mBucketLabel);
552 return super.createHeaderLabel(item);
555 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
556 sectionCreator = new LocalizedCompare<Song>(context) {
558 public String getString(Song item) {
559 return item.mArtistName;
563 public String createHeaderLabel(Song item) {
564 if (item.mBucketLabel != null) {
565 return super.createHeaderLabel(item.mBucketLabel);
568 return super.createHeaderLabel(item);
571 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_DURATION)) {
572 sectionCreator = new DurationCompare<Song>(context) {
574 public int getInt(Song item) {
575 return item.mDuration;
578 } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_YEAR)) {
579 sectionCreator = new SectionCreatorUtils.IntCompare<Song>() {
581 public int getInt(Song item) {
586 public String createHeaderLabel(Song item) {
587 // I have seen tracks in my library where it would return 0 or 2
588 // so have this check to return a more friendly label in that case
589 if (MusicUtils.isInvalidYear(item.mYear)) {
590 return context.getString(R.string.header_unknown_year);
593 return super.createHeaderLabel(item);
598 return sectionCreator;
602 * Returns an song comparison based on the current sort
603 * @param context Context for string generation
604 * @return the song comparison method
606 public static IItemCompare<SearchResult> createSearchResultComparison(final Context context) {
607 return new IItemCompare<SearchResult>() {
610 public String createSectionHeader(SearchResult first, SearchResult second) {
611 if (first == null || first.mType != second.mType) {
612 return createHeaderLabel(second);
619 public String createHeaderLabel(SearchResult item) {
620 switch (item.mType) {
622 return context.getString(R.string.page_artists);
624 return context.getString(R.string.page_albums);
626 return context.getString(R.string.page_songs);
628 return context.getString(R.string.page_playlists);
635 public String createSectionFooter(SearchResult first, SearchResult second,
636 List<SearchResult> items, int firstIndex) {
637 if (second == null ||
638 (first != null && first.mType != second.mType)) {
639 // if we don't have SEARCH_NUM_RESULTS_TO_GET # of the same type of items
640 // then we don't have enough to show the footer. For example, if we show 5
641 // items but only the last 2 items are artists, that means we only have 2
642 // so there is no point in showing the "Show All" footer
643 // We start from 1 because we don't need to count
644 // the first item itself
645 for (int i = 1; i < Config.SEARCH_NUM_RESULTS_TO_GET; i++) {
646 if (firstIndex - i < 0 || items.get(firstIndex - i).mType != first.mType) {
651 return createFooterLabel(first);
658 public String createFooterLabel(SearchResult item) {
659 switch (item.mType) {
661 return context.getString(R.string.footer_search_artists);
663 return context.getString(R.string.footer_search_albums);
665 return context.getString(R.string.footer_search_songs);
667 return context.getString(R.string.footer_search_playlists);