OSDN Git Service

Eleven: Update the lists when things change
[android-x86/packages-apps-Eleven.git] / src / com / cyngn / eleven / utils / MusicUtils.java
1 /*
2  * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3  * (the "License"); you may not use this file except in compliance with the
4  * License. You may obtain a copy of the License at
5  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6  * or agreed to in writing, software distributed under the License is
7  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8  * KIND, either express or implied. See the License for the specific language
9  * governing permissions and limitations under the License.
10  */
11
12 package com.cyngn.eleven.utils;
13
14 import android.app.Activity;
15 import android.content.ComponentName;
16 import android.content.ContentResolver;
17 import android.content.ContentUris;
18 import android.content.ContentValues;
19 import android.content.Context;
20 import android.content.ContextWrapper;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.IBinder;
26 import android.os.RemoteException;
27 import android.provider.BaseColumns;
28 import android.provider.MediaStore;
29 import android.provider.MediaStore.Audio.AlbumColumns;
30 import android.provider.MediaStore.Audio.ArtistColumns;
31 import android.provider.MediaStore.Audio.AudioColumns;
32 import android.provider.MediaStore.Audio.Playlists;
33 import android.provider.MediaStore.Audio.PlaylistsColumns;
34 import android.provider.MediaStore.MediaColumns;
35 import android.provider.Settings;
36 import android.util.Log;
37 import android.view.Menu;
38
39 import com.cyngn.eleven.Config;
40 import com.cyngn.eleven.Config.SmartPlaylistType;
41 import com.cyngn.eleven.IElevenService;
42 import com.cyngn.eleven.MusicPlaybackService;
43 import com.cyngn.eleven.R;
44 import com.cyngn.eleven.loaders.LastAddedLoader;
45 import com.cyngn.eleven.loaders.PlaylistLoader;
46 import com.cyngn.eleven.loaders.PlaylistSongLoader;
47 import com.cyngn.eleven.loaders.SongLoader;
48 import com.cyngn.eleven.loaders.TopTracksLoader;
49 import com.cyngn.eleven.menu.FragmentMenuItems;
50 import com.cyngn.eleven.model.AlbumArtistDetails;
51 import com.cyngn.eleven.provider.RecentStore;
52 import com.cyngn.eleven.provider.SongPlayCount;
53 import com.devspark.appmsg.AppMsg;
54
55 import java.io.File;
56 import java.util.Arrays;
57 import java.util.WeakHashMap;
58
59 /**
60  * A collection of helpers directly related to music or Apollo's service.
61  *
62  * @author Andrew Neal (andrewdneal@gmail.com)
63  */
64 public final class MusicUtils {
65
66     public static IElevenService mService = null;
67
68     private static int sForegroundActivities = 0;
69
70     private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
71
72     private static final long[] sEmptyList;
73
74     private static ContentValues[] mContentValuesCache = null;
75
76     private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
77
78     public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
79                     + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
80
81     static {
82         mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
83         sEmptyList = new long[0];
84     }
85
86     /* This class is never initiated */
87     public MusicUtils() {
88     }
89
90     /**
91      * @param context The {@link Context} to use
92      * @param callback The {@link ServiceConnection} to use
93      * @return The new instance of {@link ServiceToken}
94      */
95     public static final ServiceToken bindToService(final Context context,
96             final ServiceConnection callback) {
97         Activity realActivity = ((Activity)context).getParent();
98         if (realActivity == null) {
99             realActivity = (Activity)context;
100         }
101         final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
102         contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
103         final ServiceBinder binder = new ServiceBinder(callback);
104         if (contextWrapper.bindService(
105                 new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
106             mConnectionMap.put(contextWrapper, binder);
107             return new ServiceToken(contextWrapper);
108         }
109         return null;
110     }
111
112     /**
113      * @param token The {@link ServiceToken} to unbind from
114      */
115     public static void unbindFromService(final ServiceToken token) {
116         if (token == null) {
117             return;
118         }
119         final ContextWrapper mContextWrapper = token.mWrappedContext;
120         final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
121         if (mBinder == null) {
122             return;
123         }
124         mContextWrapper.unbindService(mBinder);
125         if (mConnectionMap.isEmpty()) {
126             mService = null;
127         }
128     }
129
130     public static final class ServiceBinder implements ServiceConnection {
131         private final ServiceConnection mCallback;
132
133         /**
134          * Constructor of <code>ServiceBinder</code>
135          *
136          * @param context The {@link ServiceConnection} to use
137          */
138         public ServiceBinder(final ServiceConnection callback) {
139             mCallback = callback;
140         }
141
142         @Override
143         public void onServiceConnected(final ComponentName className, final IBinder service) {
144             mService = IElevenService.Stub.asInterface(service);
145             if (mCallback != null) {
146                 mCallback.onServiceConnected(className, service);
147             }
148         }
149
150         @Override
151         public void onServiceDisconnected(final ComponentName className) {
152             if (mCallback != null) {
153                 mCallback.onServiceDisconnected(className);
154             }
155             mService = null;
156         }
157     }
158
159     public static final class ServiceToken {
160         public ContextWrapper mWrappedContext;
161
162         /**
163          * Constructor of <code>ServiceToken</code>
164          *
165          * @param context The {@link ContextWrapper} to use
166          */
167         public ServiceToken(final ContextWrapper context) {
168             mWrappedContext = context;
169         }
170     }
171
172     /**
173      * Used to make number of labels for the number of artists, albums, songs,
174      * genres, and playlists.
175      *
176      * @param context The {@link Context} to use.
177      * @param pluralInt The ID of the plural string to use.
178      * @param number The number of artists, albums, songs, genres, or playlists.
179      * @return A {@link String} used as a label for the number of artists,
180      *         albums, songs, genres, and playlists.
181      */
182     public static final String makeLabel(final Context context, final int pluralInt,
183             final int number) {
184         return context.getResources().getQuantityString(pluralInt, number, number);
185     }
186
187     /**
188      * * Used to create a formatted time string for the duration of tracks.
189      *
190      * @param context The {@link Context} to use.
191      * @param secs The track in seconds.
192      * @return Duration of a track that's properly formatted.
193      */
194     public static final String makeShortTimeString(final Context context, long secs) {
195         long hours, mins;
196
197         hours = secs / 3600;
198         secs %= 3600;
199         mins = secs / 60;
200         secs %= 60;
201
202         final String durationFormat = context.getResources().getString(
203                 hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
204         return String.format(durationFormat, hours, mins, secs);
205     }
206
207     /**
208      * * Used to create a formatted time string in the format of #d #h #m #s
209      *
210      * @param context The {@link Context} to use.
211      * @param secs The duration seconds.
212      * @return Duration properly formatted in #d #h #m #s format
213      */
214     public static final String makeLongTimeString(final Context context, long secs) {
215         long days, hours, mins;
216
217         days = secs / (3600 * 24);
218         secs %= (3600 * 24);
219         hours = secs / 3600;
220         secs %= 3600;
221         mins = secs / 60;
222         secs %= 60;
223
224         int stringId = R.string.duration_mins;
225         if (days != 0) {
226             stringId = R.string.duration_days;
227         } else if (hours != 0) {
228             stringId = R.string.duration_hours;
229         }
230
231         final String durationFormat = context.getResources().getString(stringId);
232         return String.format(durationFormat, days, hours, mins, secs);
233     }
234
235     /**
236      * Used to combine two strings with some kind of separator in between
237      *
238      * @param context The {@link Context} to use.
239      * @param first string to combine
240      * @param second string to combine
241      * @return the combined string
242      */
243     public static final String makeCombinedString(final Context context, final String first,
244                                                   final String second) {
245         final String formatter = context.getResources().getString(R.string.combine_two_strings);
246         return String.format(formatter, first, second);
247     }
248
249     /**
250      * Changes to the next track
251      */
252     public static void next() {
253         try {
254             if (mService != null) {
255                 mService.next();
256             }
257         } catch (final RemoteException ignored) {
258         }
259     }
260
261     /**
262      * Changes to the next track asynchronously
263      */
264     public static void asyncNext(final Context context) {
265         final Intent previous = new Intent(context, MusicPlaybackService.class);
266         previous.setAction(MusicPlaybackService.NEXT_ACTION);
267         context.startService(previous);
268     }
269
270     /**
271      * Changes to the previous track.
272      *
273      * @NOTE The AIDL isn't used here in order to properly use the previous
274      *       action. When the user is shuffling, because {@link
275      *       MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
276      *       be able to travel to the previously skipped track. To remedy this,
277      *       {@link MusicPlaybackService.#openCurrent()} is called in {@link
278      *       MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
279      *       is called here to specifically invoke the onStartCommand used by
280      *       {@link MusicPlaybackService}, which states if the current position
281      *       less than 2000 ms, start the track over, otherwise move to the
282      *       previously listened track.
283      */
284     public static void previous(final Context context, final boolean force) {
285         final Intent previous = new Intent(context, MusicPlaybackService.class);
286         if (force) {
287             previous.setAction(MusicPlaybackService.PREVIOUS_FORCE_ACTION);
288         } else {
289             previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
290         }
291         context.startService(previous);
292     }
293
294     /**
295      * Plays or pauses the music.
296      */
297     public static void playOrPause() {
298         try {
299             if (mService != null) {
300                 if (mService.isPlaying()) {
301                     mService.pause();
302                 } else {
303                     mService.play();
304                 }
305             }
306         } catch (final Exception ignored) {
307         }
308     }
309
310     /**
311      * Cycles through the repeat options.
312      */
313     public static void cycleRepeat() {
314         try {
315             if (mService != null) {
316                 switch (mService.getRepeatMode()) {
317                     case MusicPlaybackService.REPEAT_NONE:
318                         mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
319                         break;
320                     case MusicPlaybackService.REPEAT_ALL:
321                         mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
322                         if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
323                             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
324                         }
325                         break;
326                     default:
327                         mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
328                         break;
329                 }
330             }
331         } catch (final RemoteException ignored) {
332         }
333     }
334
335     /**
336      * Cycles through the shuffle options.
337      */
338     public static void cycleShuffle() {
339         try {
340             if (mService != null) {
341                 switch (mService.getShuffleMode()) {
342                     case MusicPlaybackService.SHUFFLE_NONE:
343                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
344                         if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
345                             mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
346                         }
347                         break;
348                     case MusicPlaybackService.SHUFFLE_NORMAL:
349                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
350                         break;
351                     case MusicPlaybackService.SHUFFLE_AUTO:
352                         mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
353                         break;
354                     default:
355                         break;
356                 }
357             }
358         } catch (final RemoteException ignored) {
359         }
360     }
361
362     /**
363      * @return True if we're playing music, false otherwise.
364      */
365     public static final boolean isPlaying() {
366         if (mService != null) {
367             try {
368                 return mService.isPlaying();
369             } catch (final RemoteException ignored) {
370             }
371         }
372         return false;
373     }
374
375     /**
376      * @return The current shuffle mode.
377      */
378     public static final int getShuffleMode() {
379         if (mService != null) {
380             try {
381                 return mService.getShuffleMode();
382             } catch (final RemoteException ignored) {
383             }
384         }
385         return 0;
386     }
387
388     /**
389      * @return The current repeat mode.
390      */
391     public static final int getRepeatMode() {
392         if (mService != null) {
393             try {
394                 return mService.getRepeatMode();
395             } catch (final RemoteException ignored) {
396             }
397         }
398         return 0;
399     }
400
401     /**
402      * @return The current track name.
403      */
404     public static final String getTrackName() {
405         if (mService != null) {
406             try {
407                 return mService.getTrackName();
408             } catch (final RemoteException ignored) {
409             }
410         }
411         return null;
412     }
413
414     /**
415      * @return The current artist name.
416      */
417     public static final String getArtistName() {
418         if (mService != null) {
419             try {
420                 return mService.getArtistName();
421             } catch (final RemoteException ignored) {
422             }
423         }
424         return null;
425     }
426
427     /**
428      * @return The current album name.
429      */
430     public static final String getAlbumName() {
431         if (mService != null) {
432             try {
433                 return mService.getAlbumName();
434             } catch (final RemoteException ignored) {
435             }
436         }
437         return null;
438     }
439
440     /**
441      * @return The current album Id.
442      */
443     public static final long getCurrentAlbumId() {
444         if (mService != null) {
445             try {
446                 return mService.getAlbumId();
447             } catch (final RemoteException ignored) {
448             }
449         }
450         return -1;
451     }
452
453     /**
454      * @return The current song Id.
455      */
456     public static final long getCurrentAudioId() {
457         if (mService != null) {
458             try {
459                 return mService.getAudioId();
460             } catch (final RemoteException ignored) {
461             }
462         }
463         return -1;
464     }
465
466     /**
467      * @return The next song Id.
468      */
469     public static final long getNextAudioId() {
470         if (mService != null) {
471             try {
472                 return mService.getNextAudioId();
473             } catch (final RemoteException ignored) {
474             }
475         }
476         return -1;
477     }
478
479     /**
480      * @return The previous song Id.
481      */
482     public static final long getPreviousAudioId() {
483         if (mService != null) {
484             try {
485                 return mService.getPreviousAudioId();
486             } catch (final RemoteException ignored) {
487             }
488         }
489         return -1;
490     }
491
492     /**
493      * @return The current artist Id.
494      */
495     public static final long getCurrentArtistId() {
496         if (mService != null) {
497             try {
498                 return mService.getArtistId();
499             } catch (final RemoteException ignored) {
500             }
501         }
502         return -1;
503     }
504
505     /**
506      * @return The audio session Id.
507      */
508     public static final int getAudioSessionId() {
509         if (mService != null) {
510             try {
511                 return mService.getAudioSessionId();
512             } catch (final RemoteException ignored) {
513             }
514         }
515         return -1;
516     }
517
518     /**
519      * @return The queue.
520      */
521     public static final long[] getQueue() {
522         try {
523             if (mService != null) {
524                 return mService.getQueue();
525             } else {
526             }
527         } catch (final RemoteException ignored) {
528         }
529         return sEmptyList;
530     }
531
532     /**
533      * @return The position of the current track in the queue.
534      */
535     public static final int getQueuePosition() {
536         try {
537             if (mService != null) {
538                 return mService.getQueuePosition();
539             }
540         } catch (final RemoteException ignored) {
541         }
542         return 0;
543     }
544
545     /**
546      * @return The queue history size
547      */
548     public static final int getQueueHistorySize() {
549         if (mService != null) {
550             try {
551                 return mService.getQueueHistorySize();
552             } catch (final RemoteException ignored) {
553             }
554         }
555         return 0;
556     }
557
558     /**
559      * @return The queue history
560      */
561     public static final int[] getQueueHistoryList() {
562         if (mService != null) {
563             try {
564                 return mService.getQueueHistoryList();
565             } catch (final RemoteException ignored) {
566             }
567         }
568         return null;
569     }
570
571     /**
572      * @param id The ID of the track to remove.
573      * @return removes track from a playlist or the queue.
574      */
575     public static final int removeTrack(final long id) {
576         try {
577             if (mService != null) {
578                 return mService.removeTrack(id);
579             }
580         } catch (final RemoteException ingored) {
581         }
582         return 0;
583     }
584
585     /**
586      * @param cursor The {@link Cursor} used to perform our query.
587      * @return The song list for a MIME type.
588      */
589     public static final long[] getSongListForCursor(Cursor cursor) {
590         if (cursor == null) {
591             return sEmptyList;
592         }
593         final int len = cursor.getCount();
594         final long[] list = new long[len];
595         cursor.moveToFirst();
596         int columnIndex = -1;
597         try {
598             columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
599         } catch (final IllegalArgumentException notaplaylist) {
600             columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
601         }
602         for (int i = 0; i < len; i++) {
603             list[i] = cursor.getLong(columnIndex);
604             cursor.moveToNext();
605         }
606         cursor.close();
607         cursor = null;
608         return list;
609     }
610
611     /**
612      * @param context The {@link Context} to use.
613      * @param id The ID of the artist.
614      * @return The song list for an artist.
615      */
616     public static final long[] getSongListForArtist(final Context context, final long id) {
617         final String[] projection = new String[] {
618             BaseColumns._ID
619         };
620         final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
621                 + AudioColumns.IS_MUSIC + "=1";
622         Cursor cursor = context.getContentResolver().query(
623                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
624                 AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
625         if (cursor != null) {
626             final long[] mList = getSongListForCursor(cursor);
627             cursor.close();
628             cursor = null;
629             return mList;
630         }
631         return sEmptyList;
632     }
633
634     /**
635      * @param context The {@link Context} to use.
636      * @param id The ID of the album.
637      * @return The song list for an album.
638      */
639     public static final long[] getSongListForAlbum(final Context context, final long id) {
640         final String[] projection = new String[] {
641             BaseColumns._ID
642         };
643         final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
644                 + "=1";
645         Cursor cursor = context.getContentResolver().query(
646                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
647                 AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
648         if (cursor != null) {
649             final long[] mList = getSongListForCursor(cursor);
650             cursor.close();
651             cursor = null;
652             return mList;
653         }
654         return sEmptyList;
655     }
656
657     /**
658      * Plays songs by an artist.
659      *
660      * @param context The {@link Context} to use.
661      * @param artistId The artist Id.
662      * @param position Specify where to start.
663      */
664     public static void playArtist(final Context context, final long artistId, int position) {
665         final long[] artistList = getSongListForArtist(context, artistId);
666         if (artistList != null) {
667             playAll(context, artistList, position, false);
668         }
669     }
670
671     /**
672      * @param context The {@link Context} to use.
673      * @param id The ID of the genre.
674      * @return The song list for an genre.
675      */
676     public static final long[] getSongListForGenre(final Context context, final long id) {
677         final String[] projection = new String[] {
678             BaseColumns._ID
679         };
680         final StringBuilder selection = new StringBuilder();
681         selection.append(AudioColumns.IS_MUSIC + "=1");
682         selection.append(" AND " + MediaColumns.TITLE + "!=''");
683         final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
684         Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
685                 null, null);
686         if (cursor != null) {
687             final long[] mList = getSongListForCursor(cursor);
688             cursor.close();
689             cursor = null;
690             return mList;
691         }
692         return sEmptyList;
693     }
694
695     /**
696      * @param context The {@link Context} to use
697      * @param uri The source of the file
698      */
699     public static void playFile(final Context context, final Uri uri) {
700         if (uri == null || mService == null) {
701             return;
702         }
703
704         // If this is a file:// URI, just use the path directly instead
705         // of going through the open-from-filedescriptor codepath.
706         String filename;
707         String scheme = uri.getScheme();
708         if ("file".equals(scheme)) {
709             filename = uri.getPath();
710         } else {
711             filename = uri.toString();
712         }
713
714         try {
715             mService.stop();
716             mService.openFile(filename);
717             mService.play();
718         } catch (final RemoteException ignored) {
719         }
720     }
721
722     /**
723      * @param context The {@link Context} to use.
724      * @param list The list of songs to play.
725      * @param position Specify where to start.
726      * @param forceShuffle True to force a shuffle, false otherwise.
727      */
728     public static void playAll(final Context context, final long[] list, int position,
729             final boolean forceShuffle) {
730         if (list == null || list.length == 0 || mService == null) {
731             return;
732         }
733         try {
734             if (forceShuffle) {
735                 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
736             } else {
737                 mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
738             }
739             final long currentId = mService.getAudioId();
740             final int currentQueuePosition = getQueuePosition();
741             if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
742                 final long[] playlist = getQueue();
743                 if (Arrays.equals(list, playlist)) {
744                     mService.play();
745                     return;
746                 }
747             }
748             if (position < 0) {
749                 position = 0;
750             }
751             mService.open(list, forceShuffle ? -1 : position);
752             mService.play();
753         } catch (final RemoteException ignored) {
754         }
755     }
756
757     /**
758      * @param list The list to enqueue.
759      */
760     public static void playNext(final long[] list) {
761         if (mService == null) {
762             return;
763         }
764         try {
765             mService.enqueue(list, MusicPlaybackService.NEXT);
766         } catch (final RemoteException ignored) {
767         }
768     }
769
770     /**
771      * @param context The {@link Context} to use.
772      */
773     public static void shuffleAll(final Context context) {
774         Cursor cursor = SongLoader.makeSongCursor(context, null);
775         final long[] mTrackList = getSongListForCursor(cursor);
776         final int position = 0;
777         if (mTrackList.length == 0 || mService == null) {
778             return;
779         }
780         try {
781             mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
782             final long mCurrentId = mService.getAudioId();
783             final int mCurrentQueuePosition = getQueuePosition();
784             if (position != -1 && mCurrentQueuePosition == position
785                     && mCurrentId == mTrackList[position]) {
786                 final long[] mPlaylist = getQueue();
787                 if (Arrays.equals(mTrackList, mPlaylist)) {
788                     mService.play();
789                     return;
790                 }
791             }
792             mService.open(mTrackList, -1);
793             mService.play();
794             cursor.close();
795             cursor = null;
796         } catch (final RemoteException ignored) {
797         }
798     }
799
800     /**
801      * Returns The ID for a playlist.
802      *
803      * @param context The {@link Context} to use.
804      * @param name The name of the playlist.
805      * @return The ID for a playlist.
806      */
807     public static final long getIdForPlaylist(final Context context, final String name) {
808         Cursor cursor = context.getContentResolver().query(
809                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
810                     BaseColumns._ID
811                 }, PlaylistsColumns.NAME + "=?", new String[] {
812                     name
813                 }, PlaylistsColumns.NAME);
814         int id = -1;
815         if (cursor != null) {
816             cursor.moveToFirst();
817             if (!cursor.isAfterLast()) {
818                 id = cursor.getInt(0);
819             }
820             cursor.close();
821             cursor = null;
822         }
823         return id;
824     }
825
826     /**
827      * Returns the Id for an artist.
828      *
829      * @param context The {@link Context} to use.
830      * @param name The name of the artist.
831      * @return The ID for an artist.
832      */
833     public static final long getIdForArtist(final Context context, final String name) {
834         Cursor cursor = context.getContentResolver().query(
835                 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{
836                         BaseColumns._ID
837                 }, ArtistColumns.ARTIST + "=?", new String[]{
838                         name
839                 }, ArtistColumns.ARTIST);
840         int id = -1;
841         if (cursor != null) {
842             cursor.moveToFirst();
843             if (!cursor.isAfterLast()) {
844                 id = cursor.getInt(0);
845             }
846             cursor.close();
847             cursor = null;
848         }
849         return id;
850     }
851
852     /**
853      * Returns the ID for an album.
854      *
855      * @param context The {@link Context} to use.
856      * @param albumName The name of the album.
857      * @param artistName The name of the artist
858      * @return The ID for an album.
859      */
860     public static final long getIdForAlbum(final Context context, final String albumName,
861             final String artistName) {
862         Cursor cursor = context.getContentResolver().query(
863                 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
864                     BaseColumns._ID
865                 }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
866                     albumName, artistName
867                 }, AlbumColumns.ALBUM);
868         int id = -1;
869         if (cursor != null) {
870             cursor.moveToFirst();
871             if (!cursor.isAfterLast()) {
872                 id = cursor.getInt(0);
873             }
874             cursor.close();
875             cursor = null;
876         }
877         return id;
878     }
879
880     /**
881      * Plays songs from an album.
882      *
883      * @param context The {@link Context} to use.
884      * @param albumId The album Id.
885      * @param position Specify where to start.
886      */
887     public static void playAlbum(final Context context, final long albumId, int position) {
888         final long[] albumList = getSongListForAlbum(context, albumId);
889         if (albumList != null) {
890             playAll(context, albumList, position, false);
891         }
892     }
893
894     /*  */
895     public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
896         if (offset + len > ids.length) {
897             len = ids.length - offset;
898         }
899
900         if (mContentValuesCache == null || mContentValuesCache.length != len) {
901             mContentValuesCache = new ContentValues[len];
902         }
903         for (int i = 0; i < len; i++) {
904             if (mContentValuesCache[i] == null) {
905                 mContentValuesCache[i] = new ContentValues();
906             }
907             mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
908             mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
909         }
910     }
911
912     /**
913      * @param context The {@link Context} to use.
914      * @param name The name of the new playlist.
915      * @return A new playlist ID.
916      */
917     public static final long createPlaylist(final Context context, final String name) {
918         if (name != null && name.length() > 0) {
919             final ContentResolver resolver = context.getContentResolver();
920             final String[] projection = new String[] {
921                 PlaylistsColumns.NAME
922             };
923             final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
924             Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
925                     projection, selection, null, null);
926             if (cursor.getCount() <= 0) {
927                 final ContentValues values = new ContentValues(1);
928                 values.put(PlaylistsColumns.NAME, name);
929                 final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
930                         values);
931                 return Long.parseLong(uri.getLastPathSegment());
932             }
933             if (cursor != null) {
934                 cursor.close();
935                 cursor = null;
936             }
937             return -1;
938         }
939         return -1;
940     }
941
942     /**
943      * @param context The {@link Context} to use.
944      * @param playlistId The playlist ID.
945      */
946     public static void clearPlaylist(final Context context, final int playlistId) {
947         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
948         context.getContentResolver().delete(uri, null, null);
949         return;
950     }
951
952     /**
953      * @param context The {@link Context} to use.
954      * @param ids The id of the song(s) to add.
955      * @param playlistid The id of the playlist being added to.
956      */
957     public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
958         final int size = ids.length;
959         final ContentResolver resolver = context.getContentResolver();
960         final String[] projection = new String[] {
961             "count(*)"
962         };
963         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
964         Cursor cursor = resolver.query(uri, projection, null, null, null);
965         cursor.moveToFirst();
966         final int base = cursor.getInt(0);
967         cursor.close();
968         cursor = null;
969         int numinserted = 0;
970         for (int offSet = 0; offSet < size; offSet += 1000) {
971             makeInsertItems(ids, offSet, 1000, base);
972             numinserted += resolver.bulkInsert(uri, mContentValuesCache);
973         }
974         final String message = context.getResources().getQuantityString(
975                 R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
976         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
977         playlistChanged();
978     }
979
980     /**
981      * Removes a single track from a given playlist
982      * @param context The {@link Context} to use.
983      * @param id The id of the song to remove.
984      * @param playlistId The id of the playlist being removed from.
985      */
986     public static void removeFromPlaylist(final Context context, final long id,
987             final long playlistId) {
988         final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
989         final ContentResolver resolver = context.getContentResolver();
990         resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
991             Long.toString(id)
992         });
993         final String message = context.getResources().getQuantityString(
994                 R.plurals.NNNtracksfromplaylist, 1, 1);
995         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
996         playlistChanged();
997     }
998
999     /**
1000      * @param context The {@link Context} to use.
1001      * @param list The list to enqueue.
1002      */
1003     public static void addToQueue(final Context context, final long[] list) {
1004         if (mService == null) {
1005             return;
1006         }
1007         try {
1008             mService.enqueue(list, MusicPlaybackService.LAST);
1009             final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
1010             AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1011         } catch (final RemoteException ignored) {
1012         }
1013     }
1014
1015     /**
1016      * @param context The {@link Context} to use
1017      * @param id The song ID.
1018      */
1019     public static void setRingtone(final Context context, final long id) {
1020         final ContentResolver resolver = context.getContentResolver();
1021         final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1022         try {
1023             final ContentValues values = new ContentValues(2);
1024             values.put(AudioColumns.IS_RINGTONE, "1");
1025             values.put(AudioColumns.IS_ALARM, "1");
1026             resolver.update(uri, values, null, null);
1027         } catch (final UnsupportedOperationException ingored) {
1028             return;
1029         }
1030
1031         final String[] projection = new String[] {
1032                 BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
1033         };
1034
1035         final String selection = BaseColumns._ID + "=" + id;
1036         Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
1037                 selection, null, null);
1038         try {
1039             if (cursor != null && cursor.getCount() == 1) {
1040                 cursor.moveToFirst();
1041                 Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
1042                 final String message = context.getString(R.string.set_as_ringtone,
1043                         cursor.getString(2));
1044                 AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1045             }
1046         } finally {
1047             if (cursor != null) {
1048                 cursor.close();
1049                 cursor = null;
1050             }
1051         }
1052     }
1053
1054     public static final String getSongCountForAlbum(final Context context, final long id) {
1055         Integer i = getSongCountForAlbumInt(context, id);
1056         return i == null ? null : Integer.toString(i);
1057     }
1058
1059     /**
1060      * @param context The {@link Context} to use.
1061      * @param id The id of the album.
1062      * @return The song count for an album.
1063      */
1064     public static final Integer getSongCountForAlbumInt(final Context context, final long id) {
1065         if (id == -1) {
1066             return null;
1067         }
1068         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1069         Cursor cursor = context.getContentResolver().query(uri, new String[] {
1070                     AlbumColumns.NUMBER_OF_SONGS
1071                 }, null, null, null);
1072         Integer songCount = null;
1073         if (cursor != null) {
1074             cursor.moveToFirst();
1075             if (!cursor.isAfterLast()) {
1076                 if(!cursor.isNull(0)) {
1077                     songCount = cursor.getInt(0);
1078                 }
1079             }
1080             cursor.close();
1081             cursor = null;
1082         }
1083         return songCount;
1084     }
1085
1086     /**
1087      * Gets the number of songs for a playlist
1088      * @param context The {@link Context} to use.
1089      * @param playlistId the id of the playlist
1090      * @return the # of songs in the playlist
1091      */
1092     public static final int getSongCountForPlaylist(final Context context, final long playlistId) {
1093         Cursor c = context.getContentResolver().query(
1094                 MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
1095                 new String[]{BaseColumns._ID}, MusicUtils.MUSIC_ONLY_SELECTION, null, null);
1096
1097         if (c != null && c.moveToFirst()) {
1098             int count = c.getCount();
1099             c.close();
1100             c = null;
1101             return count;
1102         }
1103
1104         return 0;
1105     }
1106
1107     public static final AlbumArtistDetails getAlbumArtDetails(final Context context, final long trackId) {
1108         final StringBuilder selection = new StringBuilder();
1109         selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1");
1110         selection.append(" AND " + BaseColumns._ID + " = '" + trackId + "'");
1111
1112         Cursor cursor = context.getContentResolver().query(
1113             MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1114             new String[] {
1115                     /* 0 */
1116                 MediaStore.Audio.AudioColumns.ALBUM_ID,
1117                     /* 1 */
1118                 MediaStore.Audio.AudioColumns.ALBUM,
1119                     /* 2 */
1120                 MediaStore.Audio.AlbumColumns.ARTIST,
1121             }, selection.toString(), null, null
1122         );
1123
1124         if (!cursor.moveToFirst()) {
1125             cursor.close();
1126             return null;
1127         }
1128
1129         AlbumArtistDetails result = new AlbumArtistDetails();
1130         result.mAudioId = trackId;
1131         result.mAlbumId = cursor.getLong(0);
1132         result.mAlbumName = cursor.getString(1);
1133         result.mArtistName = cursor.getString(2);
1134         cursor.close();
1135
1136         return result;
1137     }
1138
1139     /**
1140      * @param context The {@link Context} to use.
1141      * @param id The id of the album.
1142      * @return The release date for an album.
1143      */
1144     public static final String getReleaseDateForAlbum(final Context context, final long id) {
1145         if (id == -1) {
1146             return null;
1147         }
1148         Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
1149         Cursor cursor = context.getContentResolver().query(uri, new String[] {
1150                     AlbumColumns.FIRST_YEAR
1151                 }, null, null, null);
1152         String releaseDate = null;
1153         if (cursor != null) {
1154             cursor.moveToFirst();
1155             if (!cursor.isAfterLast()) {
1156                 releaseDate = cursor.getString(0);
1157             }
1158             cursor.close();
1159             cursor = null;
1160         }
1161         return releaseDate;
1162     }
1163
1164     /**
1165      * @return The path to the currently playing file as {@link String}
1166      */
1167     public static final String getFilePath() {
1168         try {
1169             if (mService != null) {
1170                 return mService.getPath();
1171             }
1172         } catch (final RemoteException ignored) {
1173         }
1174         return null;
1175     }
1176
1177     /**
1178      * @param from The index the item is currently at.
1179      * @param to The index the item is moving to.
1180      */
1181     public static void moveQueueItem(final int from, final int to) {
1182         try {
1183             if (mService != null) {
1184                 mService.moveQueueItem(from, to);
1185             } else {
1186             }
1187         } catch (final RemoteException ignored) {
1188         }
1189     }
1190
1191     /**
1192      * @param context The {@link Context} to sue
1193      * @param playlistId The playlist Id
1194      * @return The track list for a playlist
1195      */
1196     public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
1197         Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(context, playlistId);
1198
1199         if (cursor != null) {
1200             final long[] list = getSongListForCursor(cursor);
1201             cursor.close();
1202             cursor = null;
1203             return list;
1204         }
1205         return sEmptyList;
1206     }
1207
1208     /**
1209      * Plays a user created playlist.
1210      *
1211      * @param context The {@link Context} to use.
1212      * @param playlistId The playlist Id.
1213      */
1214     public static void playPlaylist(final Context context, final long playlistId) {
1215         final long[] playlistList = getSongListForPlaylist(context, playlistId);
1216         if (playlistList != null) {
1217             playAll(context, playlistList, -1, false);
1218         }
1219     }
1220
1221     /**
1222      * @param context The {@link Context} to use
1223      * @param type The Smart Playlist Type
1224      * @return The song list for the last added playlist
1225      */
1226     public static final long[] getSongListForSmartPlaylist(final Context context,
1227                                                            final SmartPlaylistType type) {
1228         Cursor cursor = null;
1229         try {
1230             switch (type) {
1231                 case LastAdded:
1232                     cursor = LastAddedLoader.makeLastAddedCursor(context);
1233                     break;
1234                 case RecentlyPlayed:
1235                     cursor = TopTracksLoader.makeRecentTracksCursor(context);
1236                     break;
1237                 case TopTracks:
1238                     cursor = TopTracksLoader.makeTopTracksCursor(context);
1239                     break;
1240             }
1241             return MusicUtils.getSongListForCursor(cursor);
1242         } finally {
1243             if (cursor != null) {
1244                 cursor.close();
1245                 cursor = null;
1246             }
1247         }
1248     }
1249
1250     /**
1251      * Plays the smart playlist
1252      * @param context The {@link Context} to use
1253      * @param position the position to start playing from
1254      * @param type The Smart Playlist Type
1255      */
1256     public static void playSmartPlaylist(final Context context, final int position,
1257                                          final SmartPlaylistType type) {
1258         final long[] list = getSongListForSmartPlaylist(context, type);
1259         MusicUtils.playAll(context, list, position, false);
1260     }
1261
1262     /**
1263      * Creates a sub menu used to add items to a new playlist or an existsing
1264      * one.
1265      *
1266      * @param context The {@link Context} to use.
1267      * @param groupId The group Id of the menu.
1268      * @param menu The {@link Menu} to add to.
1269      */
1270     public static void makePlaylistMenu(final Context context, final int groupId,
1271             final Menu menu) {
1272         menu.clear();
1273         menu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
1274         Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
1275         if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
1276             while (!cursor.isAfterLast()) {
1277                 final Intent intent = new Intent();
1278                 String name = cursor.getString(1);
1279                 if (name != null) {
1280                     intent.putExtra("playlist", getIdForPlaylist(context, name));
1281                     menu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
1282                             name).setIntent(intent);
1283                 }
1284                 cursor.moveToNext();
1285             }
1286         }
1287         if (cursor != null) {
1288             cursor.close();
1289             cursor = null;
1290         }
1291     }
1292
1293     /**
1294      * Called when one of the lists should refresh or requery.
1295      */
1296     public static void refresh() {
1297         try {
1298             if (mService != null) {
1299                 mService.refresh();
1300             }
1301         } catch (final RemoteException ignored) {
1302         }
1303     }
1304
1305     /**
1306      * Called when one of playlists have changed
1307      */
1308     public static void playlistChanged() {
1309         try {
1310             if (mService != null) {
1311                 mService.playlistChanged();
1312             }
1313         } catch (final RemoteException ignored) {
1314         }
1315     }
1316
1317     /**
1318      * Seeks the current track to a desired position
1319      *
1320      * @param position The position to seek to
1321      */
1322     public static void seek(final long position) {
1323         if (mService != null) {
1324             try {
1325                 mService.seek(position);
1326             } catch (final RemoteException ignored) {
1327             }
1328         }
1329     }
1330
1331     /**
1332      * @return The current position time of the track
1333      */
1334     public static final long position() {
1335         if (mService != null) {
1336             try {
1337                 return mService.position();
1338             } catch (final RemoteException ignored) {
1339             } catch (final IllegalStateException ex) {
1340                 Log.e(MusicUtils.class.getSimpleName(), ex.getMessage());
1341             }
1342         }
1343         return 0;
1344     }
1345
1346     /**
1347      * @return The total length of the current track
1348      */
1349     public static final long duration() {
1350         if (mService != null) {
1351             try {
1352                 return mService.duration();
1353             } catch (final RemoteException ignored) {
1354             } catch (final IllegalStateException ignored) {
1355             }
1356         }
1357         return 0;
1358     }
1359
1360     /**
1361      * @param position The position to move the queue to
1362      */
1363     public static void setQueuePosition(final int position) {
1364         if (mService != null) {
1365             try {
1366                 mService.setQueuePosition(position);
1367             } catch (final RemoteException ignored) {
1368             }
1369         }
1370     }
1371
1372     /**
1373      * Clears the qeueue
1374      */
1375     public static void clearQueue() {
1376         try {
1377             mService.removeTracks(0, Integer.MAX_VALUE);
1378         } catch (final RemoteException ignored) {
1379         }
1380     }
1381
1382     /**
1383      * Used to build and show a notification when Apollo is sent into the
1384      * background
1385      *
1386      * @param context The {@link Context} to use.
1387      */
1388     public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
1389         int old = sForegroundActivities;
1390         if (inForeground) {
1391             sForegroundActivities++;
1392         } else {
1393             sForegroundActivities--;
1394         }
1395
1396         if (old == 0 || sForegroundActivities == 0) {
1397             final Intent intent = new Intent(context, MusicPlaybackService.class);
1398             intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
1399             intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
1400             context.startService(intent);
1401         }
1402     }
1403
1404     /**
1405      * Perminately deletes item(s) from the user's device
1406      *
1407      * @param context The {@link Context} to use.
1408      * @param list The item(s) to delete.
1409      */
1410     public static void deleteTracks(final Context context, final long[] list) {
1411         final String[] projection = new String[] {
1412                 BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
1413         };
1414         final StringBuilder selection = new StringBuilder();
1415         selection.append(BaseColumns._ID + " IN (");
1416         for (int i = 0; i < list.length; i++) {
1417             selection.append(list[i]);
1418             if (i < list.length - 1) {
1419                 selection.append(",");
1420             }
1421         }
1422         selection.append(")");
1423         final Cursor c = context.getContentResolver().query(
1424                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
1425                 null, null);
1426         if (c != null) {
1427             // Step 1: Remove selected tracks from the current playlist, as well
1428             // as from the album art cache
1429             c.moveToFirst();
1430             while (!c.isAfterLast()) {
1431                 // Remove from current playlist
1432                 final long id = c.getLong(0);
1433                 removeTrack(id);
1434                 // Remove the track from the play count
1435                 SongPlayCount.getInstance(context).removeItem(id);
1436                 // Remove any items in the recents database
1437                 RecentStore.getInstance(context).removeItem(id);
1438                 c.moveToNext();
1439             }
1440
1441             // Step 2: Remove selected tracks from the database
1442             context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1443                     selection.toString(), null);
1444
1445             // Step 3: Remove files from card
1446             c.moveToFirst();
1447             while (!c.isAfterLast()) {
1448                 final String name = c.getString(1);
1449                 final File f = new File(name);
1450                 try { // File.delete can throw a security exception
1451                     if (!f.delete()) {
1452                         // I'm not sure if we'd ever get here (deletion would
1453                         // have to fail, but no exception thrown)
1454                         Log.e("MusicUtils", "Failed to delete file " + name);
1455                     }
1456                     c.moveToNext();
1457                 } catch (final SecurityException ex) {
1458                     c.moveToNext();
1459                 }
1460             }
1461             c.close();
1462         }
1463
1464         final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
1465
1466         AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
1467         // We deleted a number of tracks, which could affect any number of
1468         // things
1469         // in the media content domain, so update everything.
1470         context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
1471         // Notify the lists to update
1472         refresh();
1473     }
1474
1475     /**
1476      * Simple function used to determine if the song/album year is invalid
1477      * @param year value to test
1478      * @return true if the app considers it valid
1479      */
1480     public static boolean isInvalidYear(int year) {
1481         return year < MIN_VALID_YEAR;
1482     }
1483
1484     /**
1485      * A snippet is taken from MediaStore.Audio.keyFor method
1486      * This will take a name, removes things like "the", "an", etc
1487      * as well as special characters and return it
1488      * @param name the string to trim
1489      * @return the trimmed name
1490      */
1491     public static String getTrimmedName(String name) {
1492         if (name == null || name.length() == 0) {
1493             return name;
1494         }
1495
1496         name = name.trim().toLowerCase();
1497         if (name.startsWith("the ")) {
1498             name = name.substring(4);
1499         }
1500         if (name.startsWith("an ")) {
1501             name = name.substring(3);
1502         }
1503         if (name.startsWith("a ")) {
1504             name = name.substring(2);
1505         }
1506         if (name.endsWith(", the") || name.endsWith(",the") ||
1507                 name.endsWith(", an") || name.endsWith(",an") ||
1508                 name.endsWith(", a") || name.endsWith(",a")) {
1509             name = name.substring(0, name.lastIndexOf(','));
1510         }
1511         name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
1512
1513         return name;
1514     }
1515
1516     /**
1517      * A snippet is taken from MediaStore.Audio.keyFor method
1518      * This will take a name, removes things like "the", "an", etc
1519      * as well as special characters, then find the localized label
1520      * @param name Name to get the label of
1521      * @param trimName boolean flag to run the trimmer on the name
1522      * @return the localized label of the bucket that the name falls into
1523      */
1524     public static String getLocalizedBucketLetter(String name, boolean trimName) {
1525         if (name == null || name.length() == 0) {
1526             return null;
1527         }
1528
1529         if (trimName) {
1530             name = getTrimmedName(name);
1531         }
1532
1533         if (name.length() > 0) {
1534             String lbl = LocaleUtils.getInstance().getLabel(name);
1535             // For now let's cap it to latin alphabet and the # sign
1536             // since chinese characters are resulting in " " and other random
1537             // characters but the sort doesn't match the sql sort so it is
1538             // not quite sorted
1539             if (lbl != null && lbl.length() > 0) {
1540                 char ch = lbl.charAt(0);
1541                 if (ch < 'A' && ch > 'Z' && ch != '#') {
1542                     return null;
1543                 }
1544             }
1545
1546             if (lbl != null && lbl.length() > 0) {
1547                 return lbl;
1548             }
1549         }
1550
1551         return null;
1552     }
1553
1554     /** @return true if a string is null, empty, or contains only whitespace */
1555     public static boolean isBlank(String s) {
1556         if(s == null) { return true; }
1557         if(s.isEmpty()) { return true; }
1558         for(int i = 0; i < s.length(); i++) {
1559             char c = s.charAt(i);
1560             if(!Character.isWhitespace(c)) { return false; }
1561         }
1562         return true;
1563     }
1564 }