OSDN Git Service

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