summaryrefslogtreecommitdiffstats
path: root/src/com/cyngn/eleven
diff options
context:
space:
mode:
authorLinus Lee <llee@cyngn.com>2014-08-04 15:05:06 -0700
committerLinus Lee <llee@cyngn.com>2014-08-04 15:05:06 -0700
commit5601e2a4123fa540ecb6fb484ea0547338c5cf26 (patch)
tree9237c4d3fd998e7032bf81e58aaa15fff080af4d /src/com/cyngn/eleven
parent3b7f423ddabc26ff0430e5ff9f446fb7aeaf578b (diff)
downloadandroid_packages_apps_Eleven-5601e2a4123fa540ecb6fb484ea0547338c5cf26.tar.gz
android_packages_apps_Eleven-5601e2a4123fa540ecb6fb484ea0547338c5cf26.tar.bz2
android_packages_apps_Eleven-5601e2a4123fa540ecb6fb484ea0547338c5cf26.zip
Repackaged com.andrew.apollo to com.cyngn.eleven
Change-Id: Ifc925f29f4028afd359375a1470c129b6528e74f
Diffstat (limited to 'src/com/cyngn/eleven')
-rw-r--r--src/com/cyngn/eleven/Config.java66
-rw-r--r--src/com/cyngn/eleven/ElevenApplication.java65
-rw-r--r--src/com/cyngn/eleven/IElevenService.aidl42
-rw-r--r--src/com/cyngn/eleven/MediaButtonIntentReceiver.java224
-rw-r--r--src/com/cyngn/eleven/MusicPlaybackService.java3001
-rw-r--r--src/com/cyngn/eleven/MusicStateListener.java19
-rw-r--r--src/com/cyngn/eleven/NotificationHelper.java270
-rw-r--r--src/com/cyngn/eleven/adapters/AlbumAdapter.java244
-rw-r--r--src/com/cyngn/eleven/adapters/ArtistAdapter.java226
-rw-r--r--src/com/cyngn/eleven/adapters/ArtistAlbumAdapter.java247
-rw-r--r--src/com/cyngn/eleven/adapters/GenreAdapter.java135
-rw-r--r--src/com/cyngn/eleven/adapters/PagerAdapter.java224
-rw-r--r--src/com/cyngn/eleven/adapters/PlaylistAdapter.java135
-rw-r--r--src/com/cyngn/eleven/adapters/ProfileSongAdapter.java257
-rw-r--r--src/com/cyngn/eleven/adapters/SongAdapter.java140
-rw-r--r--src/com/cyngn/eleven/appwidgets/AppWidgetBase.java32
-rw-r--r--src/com/cyngn/eleven/appwidgets/AppWidgetLarge.java190
-rw-r--r--src/com/cyngn/eleven/appwidgets/AppWidgetLargeAlternate.java234
-rw-r--r--src/com/cyngn/eleven/appwidgets/AppWidgetSmall.java196
-rw-r--r--src/com/cyngn/eleven/appwidgets/RecentWidgetProvider.java283
-rw-r--r--src/com/cyngn/eleven/appwidgets/RecentWidgetService.java241
-rw-r--r--src/com/cyngn/eleven/cache/DiskLruCache.java969
-rw-r--r--src/com/cyngn/eleven/cache/ImageCache.java780
-rw-r--r--src/com/cyngn/eleven/cache/ImageFetcher.java414
-rw-r--r--src/com/cyngn/eleven/cache/ImageWorker.java440
-rw-r--r--src/com/cyngn/eleven/cache/LruCache.java333
-rw-r--r--src/com/cyngn/eleven/dragdrop/DragSortController.java442
-rw-r--r--src/com/cyngn/eleven/dragdrop/DragSortListView.java2115
-rw-r--r--src/com/cyngn/eleven/dragdrop/SimpleFloatViewManager.java77
-rw-r--r--src/com/cyngn/eleven/format/Capitalize.java60
-rw-r--r--src/com/cyngn/eleven/format/PrefixHighlighter.java124
-rw-r--r--src/com/cyngn/eleven/lastfm/Album.java108
-rw-r--r--src/com/cyngn/eleven/lastfm/Artist.java120
-rw-r--r--src/com/cyngn/eleven/lastfm/Caller.java291
-rw-r--r--src/com/cyngn/eleven/lastfm/DomElement.java175
-rw-r--r--src/com/cyngn/eleven/lastfm/Image.java57
-rw-r--r--src/com/cyngn/eleven/lastfm/ImageHolder.java83
-rw-r--r--src/com/cyngn/eleven/lastfm/ImageSize.java31
-rw-r--r--src/com/cyngn/eleven/lastfm/ItemFactory.java44
-rw-r--r--src/com/cyngn/eleven/lastfm/ItemFactoryBuilder.java77
-rw-r--r--src/com/cyngn/eleven/lastfm/MapUtilities.java90
-rw-r--r--src/com/cyngn/eleven/lastfm/MusicEntry.java94
-rw-r--r--src/com/cyngn/eleven/lastfm/PaginatedResult.java87
-rw-r--r--src/com/cyngn/eleven/lastfm/ResponseBuilder.java209
-rw-r--r--src/com/cyngn/eleven/lastfm/Result.java138
-rw-r--r--src/com/cyngn/eleven/lastfm/StringUtilities.java176
-rw-r--r--src/com/cyngn/eleven/loaders/AlbumLoader.java116
-rw-r--r--src/com/cyngn/eleven/loaders/AlbumSongLoader.java131
-rw-r--r--src/com/cyngn/eleven/loaders/ArtistAlbumLoader.java121
-rw-r--r--src/com/cyngn/eleven/loaders/ArtistLoader.java111
-rw-r--r--src/com/cyngn/eleven/loaders/ArtistSongLoader.java131
-rw-r--r--src/com/cyngn/eleven/loaders/AsyncHandler.java43
-rw-r--r--src/com/cyngn/eleven/loaders/FavoritesLoader.java108
-rw-r--r--src/com/cyngn/eleven/loaders/GenreLoader.java101
-rw-r--r--src/com/cyngn/eleven/loaders/GenreSongLoader.java125
-rw-r--r--src/com/cyngn/eleven/loaders/LastAddedLoader.java121
-rw-r--r--src/com/cyngn/eleven/loaders/NowPlayingCursor.java291
-rw-r--r--src/com/cyngn/eleven/loaders/PlaylistLoader.java119
-rw-r--r--src/com/cyngn/eleven/loaders/PlaylistSongLoader.java136
-rw-r--r--src/com/cyngn/eleven/loaders/QueueLoader.java102
-rw-r--r--src/com/cyngn/eleven/loaders/RecentLoader.java115
-rw-r--r--src/com/cyngn/eleven/loaders/SearchLoader.java126
-rw-r--r--src/com/cyngn/eleven/loaders/SongLoader.java122
-rw-r--r--src/com/cyngn/eleven/loaders/WrappedAsyncTaskLoader.java70
-rw-r--r--src/com/cyngn/eleven/menu/BasePlaylistDialog.java179
-rw-r--r--src/com/cyngn/eleven/menu/CreateNewPlaylist.java145
-rw-r--r--src/com/cyngn/eleven/menu/DeleteDialog.java112
-rw-r--r--src/com/cyngn/eleven/menu/FragmentMenuItems.java73
-rw-r--r--src/com/cyngn/eleven/menu/PhotoSelectionDialog.java165
-rw-r--r--src/com/cyngn/eleven/menu/RenamePlaylist.java137
-rw-r--r--src/com/cyngn/eleven/model/Album.java123
-rw-r--r--src/com/cyngn/eleven/model/Artist.java112
-rw-r--r--src/com/cyngn/eleven/model/Genre.java86
-rw-r--r--src/com/cyngn/eleven/model/Playlist.java86
-rw-r--r--src/com/cyngn/eleven/model/Song.java121
-rw-r--r--src/com/cyngn/eleven/provider/FavoritesStore.java240
-rw-r--r--src/com/cyngn/eleven/provider/RecentStore.java203
-rw-r--r--src/com/cyngn/eleven/recycler/RecycleHolder.java65
-rw-r--r--src/com/cyngn/eleven/ui/MusicHolder.java161
-rw-r--r--src/com/cyngn/eleven/ui/activities/AudioPlayerActivity.java984
-rw-r--r--src/com/cyngn/eleven/ui/activities/BaseActivity.java456
-rw-r--r--src/com/cyngn/eleven/ui/activities/HomeActivity.java49
-rw-r--r--src/com/cyngn/eleven/ui/activities/ProfileActivity.java691
-rw-r--r--src/com/cyngn/eleven/ui/activities/SearchActivity.java589
-rw-r--r--src/com/cyngn/eleven/ui/activities/SettingsActivity.java203
-rw-r--r--src/com/cyngn/eleven/ui/activities/ShortcutActivity.java344
-rw-r--r--src/com/cyngn/eleven/ui/activities/ThemesActivity.java91
-rw-r--r--src/com/cyngn/eleven/ui/fragments/AlbumFragment.java465
-rw-r--r--src/com/cyngn/eleven/ui/fragments/ArtistFragment.java460
-rw-r--r--src/com/cyngn/eleven/ui/fragments/GenreFragment.java237
-rw-r--r--src/com/cyngn/eleven/ui/fragments/PlaylistFragment.java335
-rw-r--r--src/com/cyngn/eleven/ui/fragments/QueueFragment.java411
-rw-r--r--src/com/cyngn/eleven/ui/fragments/RecentFragment.java431
-rw-r--r--src/com/cyngn/eleven/ui/fragments/SongFragment.java380
-rw-r--r--src/com/cyngn/eleven/ui/fragments/ThemeFragment.java320
-rw-r--r--src/com/cyngn/eleven/ui/fragments/phone/MusicBrowserPhoneFragment.java350
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/AlbumSongFragment.java343
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/ArtistAlbumFragment.java332
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/ArtistSongFragment.java339
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/FavoriteFragment.java339
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/GenreSongFragment.java346
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/LastAddedFragment.java337
-rw-r--r--src/com/cyngn/eleven/ui/fragments/profile/PlaylistSongFragment.java411
-rw-r--r--src/com/cyngn/eleven/utils/ApolloUtils.java373
-rw-r--r--src/com/cyngn/eleven/utils/BitmapUtils.java295
-rw-r--r--src/com/cyngn/eleven/utils/Lists.java52
-rw-r--r--src/com/cyngn/eleven/utils/MusicUtils.java1369
-rw-r--r--src/com/cyngn/eleven/utils/NavUtils.java152
-rw-r--r--src/com/cyngn/eleven/utils/PreferenceUtils.java386
-rw-r--r--src/com/cyngn/eleven/utils/SortOrder.java163
-rw-r--r--src/com/cyngn/eleven/utils/ThemeUtils.java391
-rw-r--r--src/com/cyngn/eleven/widgets/AlphaPatternDrawable.java132
-rw-r--r--src/com/cyngn/eleven/widgets/AlphaTouchInterceptorOverlay.java112
-rw-r--r--src/com/cyngn/eleven/widgets/CarouselTab.java248
-rw-r--r--src/com/cyngn/eleven/widgets/ColorPanelView.java166
-rw-r--r--src/com/cyngn/eleven/widgets/ColorPickerView.java947
-rw-r--r--src/com/cyngn/eleven/widgets/ColorSchemeDialog.java225
-rw-r--r--src/com/cyngn/eleven/widgets/FrameLayoutWithOverlay.java75
-rw-r--r--src/com/cyngn/eleven/widgets/LayoutSuppressingImageView.java42
-rw-r--r--src/com/cyngn/eleven/widgets/PlayPauseButton.java102
-rw-r--r--src/com/cyngn/eleven/widgets/ProfileTabCarousel.java535
-rw-r--r--src/com/cyngn/eleven/widgets/RepeatButton.java116
-rw-r--r--src/com/cyngn/eleven/widgets/RepeatingImageButton.java216
-rw-r--r--src/com/cyngn/eleven/widgets/SeparatedListAdapter.java157
-rw-r--r--src/com/cyngn/eleven/widgets/ShowHideMasterLayout.java398
-rw-r--r--src/com/cyngn/eleven/widgets/ShuffleButton.java110
-rw-r--r--src/com/cyngn/eleven/widgets/SquareImageView.java45
-rw-r--r--src/com/cyngn/eleven/widgets/SquareView.java63
-rw-r--r--src/com/cyngn/eleven/widgets/VerticalScrollListener.java82
-rw-r--r--src/com/cyngn/eleven/widgets/theme/BottomActionBar.java57
-rw-r--r--src/com/cyngn/eleven/widgets/theme/Colorstrip.java44
-rw-r--r--src/com/cyngn/eleven/widgets/theme/HoloSelector.java76
-rw-r--r--src/com/cyngn/eleven/widgets/theme/ThemeableFrameLayout.java47
-rw-r--r--src/com/cyngn/eleven/widgets/theme/ThemeableSeekBar.java42
-rw-r--r--src/com/cyngn/eleven/widgets/theme/ThemeableTextView.java98
-rw-r--r--src/com/cyngn/eleven/widgets/theme/ThemeableTitlePageIndicator.java67
136 files changed, 34828 insertions, 0 deletions
diff --git a/src/com/cyngn/eleven/Config.java b/src/com/cyngn/eleven/Config.java
new file mode 100644
index 0000000..bd2f527
--- /dev/null
+++ b/src/com/cyngn/eleven/Config.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven;
+
+/**
+ * App-wide constants.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class Config {
+
+ /* This class is never initiated. */
+ public Config() {
+ }
+
+ /**
+ * My personal Last.fm API key, please use your own.
+ */
+ public static final String LASTFM_API_KEY = "0bec3f7ec1f914d7c960c12a916c8fb3";
+
+ /**
+ * Used to distinguish album art from artist images
+ */
+ public static final String ALBUM_ART_SUFFIX = "album";
+
+ /**
+ * The ID of an artist, album, genre, or playlist passed to the profile
+ * activity
+ */
+ public static final String ID = "id";
+
+ /**
+ * The name of an artist, album, genre, or playlist passed to the profile
+ * activity
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The name of an artist passed to the profile activity
+ */
+ public static final String ARTIST_NAME = "artist_name";
+
+ /**
+ * The year an album was released passed to the profile activity
+ */
+ public static final String ALBUM_YEAR = "album_year";
+
+ /**
+ * The MIME type passed to a the profile activity
+ */
+ public static final String MIME_TYPE = "mime_type";
+
+ /**
+ * Play from search intent
+ */
+ public static final String PLAY_FROM_SEARCH = "android.media.action.MEDIA_PLAY_FROM_SEARCH";
+}
diff --git a/src/com/cyngn/eleven/ElevenApplication.java b/src/com/cyngn/eleven/ElevenApplication.java
new file mode 100644
index 0000000..1d3d79f
--- /dev/null
+++ b/src/com/cyngn/eleven/ElevenApplication.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven;
+
+import android.app.Application;
+import android.os.StrictMode;
+
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.utils.ApolloUtils;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Used to turn off logging for jaudiotagger and free up memory when
+ * {@code #onLowMemory()} is called on pre-ICS devices. On post-ICS memory is
+ * released within {@link ImageCache}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ElevenApplication extends Application {
+ private static final boolean DEBUG = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ // Enable strict mode logging
+ enableStrictMode();
+ // Turn off logging for jaudiotagger.
+ Logger.getLogger("org.jaudiotagger").setLevel(Level.OFF);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLowMemory() {
+ ImageCache.getInstance(this).evictAll();
+ super.onLowMemory();
+ }
+
+ private void enableStrictMode() {
+ if (DEBUG) {
+ final StrictMode.ThreadPolicy.Builder threadPolicyBuilder = new StrictMode.ThreadPolicy.Builder()
+ .detectAll().penaltyLog();
+ final StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder()
+ .detectAll().penaltyLog();
+
+ threadPolicyBuilder.penaltyFlashScreen();
+ StrictMode.setThreadPolicy(threadPolicyBuilder.build());
+ StrictMode.setVmPolicy(vmPolicyBuilder.build());
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/IElevenService.aidl b/src/com/cyngn/eleven/IElevenService.aidl
new file mode 100644
index 0000000..3aa3c93
--- /dev/null
+++ b/src/com/cyngn/eleven/IElevenService.aidl
@@ -0,0 +1,42 @@
+package com.cyngn.eleven;
+
+import android.graphics.Bitmap;
+
+interface IElevenService
+{
+ void openFile(String path);
+ void open(in long [] list, int position);
+ void stop();
+ void pause();
+ void play();
+ void prev();
+ void next();
+ void enqueue(in long [] list, int action);
+ void setQueuePosition(int index);
+ void setShuffleMode(int shufflemode);
+ void setRepeatMode(int repeatmode);
+ void moveQueueItem(int from, int to);
+ void toggleFavorite();
+ void refresh();
+ boolean isFavorite();
+ boolean isPlaying();
+ long [] getQueue();
+ long duration();
+ long position();
+ long seek(long pos);
+ long getAudioId();
+ long getArtistId();
+ long getAlbumId();
+ String getArtistName();
+ String getTrackName();
+ String getAlbumName();
+ String getPath();
+ int getQueuePosition();
+ int getShuffleMode();
+ int removeTracks(int first, int last);
+ int removeTrack(long id);
+ int getRepeatMode();
+ int getMediaMountedCount();
+ int getAudioSessionId();
+}
+
diff --git a/src/com/cyngn/eleven/MediaButtonIntentReceiver.java b/src/com/cyngn/eleven/MediaButtonIntentReceiver.java
new file mode 100644
index 0000000..6c7306a
--- /dev/null
+++ b/src/com/cyngn/eleven/MediaButtonIntentReceiver.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.support.v4.content.WakefulBroadcastReceiver;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.cyngn.eleven.ui.activities.HomeActivity;
+
+/**
+ * Used to control headset playback.
+ * Single press: pause/resume
+ * Double press: next track
+ * Triple press: previous track
+ * Long press: voice search
+ */
+public class MediaButtonIntentReceiver extends WakefulBroadcastReceiver {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "MediaButtonIntentReceiver";
+
+ private static final int MSG_LONGPRESS_TIMEOUT = 1;
+ private static final int MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2;
+
+ private static final int LONG_PRESS_DELAY = 1000;
+ private static final int DOUBLE_CLICK = 800;
+
+ private static WakeLock mWakeLock = null;
+ private static int mClickCounter = 0;
+ private static long mLastClickTime = 0;
+ private static boolean mDown = false;
+ private static boolean mLaunched = false;
+
+ private static Handler mHandler = new Handler() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_LONGPRESS_TIMEOUT:
+ if (DEBUG) Log.v(TAG, "Handling longpress timeout, launched " + mLaunched);
+ if (!mLaunched) {
+ final Context context = (Context)msg.obj;
+ final Intent i = new Intent();
+ i.setClass(context, HomeActivity.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(i);
+ mLaunched = true;
+ }
+ break;
+
+ case MSG_HEADSET_DOUBLE_CLICK_TIMEOUT:
+ final int clickCount = msg.arg1;
+ final String command;
+
+ if (DEBUG) Log.v(TAG, "Handling headset click, count = " + clickCount);
+ switch (clickCount) {
+ case 1: command = MusicPlaybackService.CMDTOGGLEPAUSE; break;
+ case 2: command = MusicPlaybackService.CMDNEXT; break;
+ case 3: command = MusicPlaybackService.CMDPREVIOUS; break;
+ default: command = null; break;
+ }
+
+ if (command != null) {
+ final Context context = (Context)msg.obj;
+ startService(context, command);
+ }
+ break;
+ }
+ releaseWakeLockIfHandlerIdle();
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (DEBUG) Log.v(TAG, "Received intent: " + intent);
+ final String intentAction = intent.getAction();
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intentAction)) {
+ startService(context, MusicPlaybackService.CMDPAUSE);
+ } else if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
+ final KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ if (event == null) {
+ return;
+ }
+
+ final int keycode = event.getKeyCode();
+ final int action = event.getAction();
+ final long eventtime = event.getEventTime();
+
+ String command = null;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ command = MusicPlaybackService.CMDSTOP;
+ break;
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ command = MusicPlaybackService.CMDTOGGLEPAUSE;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ command = MusicPlaybackService.CMDNEXT;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ command = MusicPlaybackService.CMDPREVIOUS;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ command = MusicPlaybackService.CMDPAUSE;
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ command = MusicPlaybackService.CMDPLAY;
+ break;
+ }
+ if (command != null) {
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (mDown) {
+ if (MusicPlaybackService.CMDTOGGLEPAUSE.equals(command)
+ || MusicPlaybackService.CMDPLAY.equals(command)) {
+ if (mLastClickTime != 0
+ && eventtime - mLastClickTime > LONG_PRESS_DELAY) {
+ acquireWakeLockAndSendMessage(context,
+ mHandler.obtainMessage(MSG_LONGPRESS_TIMEOUT, context), 0);
+ }
+ }
+ } else if (event.getRepeatCount() == 0) {
+ // Only consider the first event in a sequence, not the repeat events,
+ // so that we don't trigger in cases where the first event went to
+ // a different app (e.g. when the user ends a phone call by
+ // long pressing the headset button)
+
+ // The service may or may not be running, but we need to send it
+ // a command.
+ if (keycode == KeyEvent.KEYCODE_HEADSETHOOK) {
+ if (eventtime - mLastClickTime >= DOUBLE_CLICK) {
+ mClickCounter = 0;
+ }
+
+ mClickCounter++;
+ if (DEBUG) Log.v(TAG, "Got headset click, count = " + mClickCounter);
+ mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT);
+
+ Message msg = mHandler.obtainMessage(
+ MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context);
+
+ long delay = mClickCounter < 3 ? DOUBLE_CLICK : 0;
+ if (mClickCounter >= 3) {
+ mClickCounter = 0;
+ }
+ mLastClickTime = eventtime;
+ acquireWakeLockAndSendMessage(context, msg, delay);
+ } else {
+ startService(context, command);
+ }
+ mLaunched = false;
+ mDown = true;
+ }
+ } else {
+ mHandler.removeMessages(MSG_LONGPRESS_TIMEOUT);
+ mDown = false;
+ }
+ if (isOrderedBroadcast()) {
+ abortBroadcast();
+ }
+ releaseWakeLockIfHandlerIdle();
+ }
+ }
+ }
+
+ private static void startService(Context context, String command) {
+ final Intent i = new Intent(context, MusicPlaybackService.class);
+ i.setAction(MusicPlaybackService.SERVICECMD);
+ i.putExtra(MusicPlaybackService.CMDNAME, command);
+ i.putExtra(MusicPlaybackService.FROM_MEDIA_BUTTON, true);
+ startWakefulService(context, i);
+ }
+
+ private static void acquireWakeLockAndSendMessage(Context context, Message msg, long delay) {
+ if (mWakeLock == null) {
+ Context appContext = context.getApplicationContext();
+ PowerManager pm = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Eleven headset button");
+ mWakeLock.setReferenceCounted(false);
+ }
+ if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what);
+ // Make sure we don't indefinitely hold the wake lock under any circumstances
+ mWakeLock.acquire(10000);
+
+ mHandler.sendMessageDelayed(msg, delay);
+ }
+
+ private static void releaseWakeLockIfHandlerIdle() {
+ if (mHandler.hasMessages(MSG_LONGPRESS_TIMEOUT)
+ || mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
+ if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock");
+ return;
+ }
+
+ if (mWakeLock != null) {
+ if (DEBUG) Log.v(TAG, "Releasing wake lock");
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/MusicPlaybackService.java b/src/com/cyngn/eleven/MusicPlaybackService.java
new file mode 100644
index 0000000..61d04a1b
--- /dev/null
+++ b/src/com/cyngn/eleven/MusicPlaybackService.java
@@ -0,0 +1,3001 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven;
+
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.RemoteControlClient;
+import android.media.audiofx.AudioEffect;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+import android.provider.MediaStore.Audio.AudioColumns;
+import android.util.Log;
+
+import com.cyngn.eleven.appwidgets.AppWidgetLarge;
+import com.cyngn.eleven.appwidgets.AppWidgetLargeAlternate;
+import com.cyngn.eleven.appwidgets.AppWidgetSmall;
+import com.cyngn.eleven.appwidgets.RecentWidgetProvider;
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.provider.RecentStore;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.TreeSet;
+
+/**
+ * A backbround {@link Service} used to keep music playing between activities
+ * and when the user moves Apollo into the background.
+ */
+@SuppressLint("NewApi")
+public class MusicPlaybackService extends Service {
+ private static final String TAG = "MusicPlaybackService";
+ private static final boolean D = false;
+
+ /**
+ * Indicates that the music has paused or resumed
+ */
+ public static final String PLAYSTATE_CHANGED = "com.cyngn.eleven.playstatechanged";
+
+ /**
+ * Indicates that music playback position within
+ * a title was changed
+ */
+ public static final String POSITION_CHANGED = "com.cyngn.eleven.positionchanged";
+
+ /**
+ * Indicates the meta data has changed in some way, like a track change
+ */
+ public static final String META_CHANGED = "com.cyngn.eleven.metachanged";
+
+ /**
+ * Indicates the queue has been updated
+ */
+ public static final String QUEUE_CHANGED = "com.cyngn.eleven.queuechanged";
+
+ /**
+ * Indicates the repeat mode chaned
+ */
+ public static final String REPEATMODE_CHANGED = "com.cyngn.eleven.repeatmodechanged";
+
+ /**
+ * Indicates the shuffle mode chaned
+ */
+ public static final String SHUFFLEMODE_CHANGED = "com.cyngn.eleven.shufflemodechanged";
+
+ /**
+ * For backwards compatibility reasons, also provide sticky
+ * broadcasts under the music package
+ */
+ public static final String ELEVEN_PACKAGE_NAME = "com.cyngn.eleven";
+ public static final String MUSIC_PACKAGE_NAME = "com.android.music";
+
+ /**
+ * Called to indicate a general service commmand. Used in
+ * {@link MediaButtonIntentReceiver}
+ */
+ public static final String SERVICECMD = "com.cyngn.eleven.musicservicecommand";
+
+ /**
+ * Called to go toggle between pausing and playing the music
+ */
+ public static final String TOGGLEPAUSE_ACTION = "com.cyngn.eleven.togglepause";
+
+ /**
+ * Called to go to pause the playback
+ */
+ public static final String PAUSE_ACTION = "com.cyngn.eleven.pause";
+
+ /**
+ * Called to go to stop the playback
+ */
+ public static final String STOP_ACTION = "com.cyngn.eleven.stop";
+
+ /**
+ * Called to go to the previous track
+ */
+ public static final String PREVIOUS_ACTION = "com.cyngn.eleven.previous";
+
+ /**
+ * Called to go to the next track
+ */
+ public static final String NEXT_ACTION = "com.cyngn.eleven.next";
+
+ /**
+ * Called to change the repeat mode
+ */
+ public static final String REPEAT_ACTION = "com.cyngn.eleven.repeat";
+
+ /**
+ * Called to change the shuffle mode
+ */
+ public static final String SHUFFLE_ACTION = "com.cyngn.eleven.shuffle";
+
+ /**
+ * Called to update the service about the foreground state of Apollo's activities
+ */
+ public static final String FOREGROUND_STATE_CHANGED = "com.cyngn.eleven.fgstatechanged";
+
+ public static final String NOW_IN_FOREGROUND = "nowinforeground";
+
+ public static final String FROM_MEDIA_BUTTON = "frommediabutton";
+
+ /**
+ * Used to easily notify a list that it should refresh. i.e. A playlist
+ * changes
+ */
+ public static final String REFRESH = "com.cyngn.eleven.refresh";
+
+ /**
+ * Used by the alarm intent to shutdown the service after being idle
+ */
+ private static final String SHUTDOWN = "com.cyngn.eleven.shutdown";
+
+ /**
+ * Called to update the remote control client
+ */
+ public static final String UPDATE_LOCKSCREEN = "com.cyngn.eleven.updatelockscreen";
+
+ public static final String CMDNAME = "command";
+
+ public static final String CMDTOGGLEPAUSE = "togglepause";
+
+ public static final String CMDSTOP = "stop";
+
+ public static final String CMDPAUSE = "pause";
+
+ public static final String CMDPLAY = "play";
+
+ public static final String CMDPREVIOUS = "previous";
+
+ public static final String CMDNEXT = "next";
+
+ public static final String CMDNOTIF = "buttonId";
+
+ private static final int IDCOLIDX = 0;
+
+ /**
+ * Moves a list to the front of the queue
+ */
+ public static final int NOW = 1;
+
+ /**
+ * Moves a list to the next position in the queue
+ */
+ public static final int NEXT = 2;
+
+ /**
+ * Moves a list to the last position in the queue
+ */
+ public static final int LAST = 3;
+
+ /**
+ * Shuffles no songs, turns shuffling off
+ */
+ public static final int SHUFFLE_NONE = 0;
+
+ /**
+ * Shuffles all songs
+ */
+ public static final int SHUFFLE_NORMAL = 1;
+
+ /**
+ * Party shuffle
+ */
+ public static final int SHUFFLE_AUTO = 2;
+
+ /**
+ * Turns repeat off
+ */
+ public static final int REPEAT_NONE = 0;
+
+ /**
+ * Repeats the current track in a list
+ */
+ public static final int REPEAT_CURRENT = 1;
+
+ /**
+ * Repeats all the tracks in a list
+ */
+ public static final int REPEAT_ALL = 2;
+
+ /**
+ * Indicates when the track ends
+ */
+ private static final int TRACK_ENDED = 1;
+
+ /**
+ * Indicates that the current track was changed the next track
+ */
+ private static final int TRACK_WENT_TO_NEXT = 2;
+
+ /**
+ * Indicates when the release the wake lock
+ */
+ private static final int RELEASE_WAKELOCK = 3;
+
+ /**
+ * Indicates the player died
+ */
+ private static final int SERVER_DIED = 4;
+
+ /**
+ * Indicates some sort of focus change, maybe a phone call
+ */
+ private static final int FOCUSCHANGE = 5;
+
+ /**
+ * Indicates to fade the volume down
+ */
+ private static final int FADEDOWN = 6;
+
+ /**
+ * Indicates to fade the volume back up
+ */
+ private static final int FADEUP = 7;
+
+ /**
+ * Idle time before stopping the foreground notfication (1 minute)
+ */
+ private static final int IDLE_DELAY = 60000;
+
+ /**
+ * Song play time used as threshold for rewinding to the beginning of the
+ * track instead of skipping to the previous track when getting the PREVIOUS
+ * command
+ */
+ private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
+
+ /**
+ * The max size allowed for the track history
+ */
+ private static final int MAX_HISTORY_SIZE = 100;
+
+ /**
+ * The columns used to retrieve any info from the current track
+ */
+ private static final String[] PROJECTION = new String[] {
+ "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
+ MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
+ MediaStore.Audio.Media.ARTIST_ID
+ };
+
+ /**
+ * The columns used to retrieve any info from the current album
+ */
+ private static final String[] ALBUM_PROJECTION = new String[] {
+ MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
+ MediaStore.Audio.Albums.LAST_YEAR
+ };
+
+ /**
+ * Keeps a mapping of the track history
+ */
+ private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
+
+ /**
+ * Used to shuffle the tracks
+ */
+ private static final Shuffler mShuffler = new Shuffler();
+
+ /**
+ * Used to save the queue as reverse hexadecimal numbers, which we can
+ * generate faster than normal decimal or hexadecimal numbers, which in
+ * turn allows us to save the playlist more often without worrying too
+ * much about performance
+ */
+ private static final char HEX_DIGITS[] = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ /**
+ * Service stub
+ */
+ private final IBinder mBinder = new ServiceStub(this);
+
+ /**
+ * 4x1 widget
+ */
+ private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
+
+ /**
+ * 4x2 widget
+ */
+ private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
+
+ /**
+ * 4x2 alternate widget
+ */
+ private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
+ .getInstance();
+
+ /**
+ * Recently listened widget
+ */
+ private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
+
+ /**
+ * The media player
+ */
+ private MultiPlayer mPlayer;
+
+ /**
+ * The path of the current file to play
+ */
+ private String mFileToPlay;
+
+ /**
+ * Keeps the service running when the screen is off
+ */
+ private WakeLock mWakeLock;
+
+ /**
+ * Alarm intent for removing the notification when nothing is playing
+ * for some time
+ */
+ private AlarmManager mAlarmManager;
+ private PendingIntent mShutdownIntent;
+ private boolean mShutdownScheduled;
+
+ /**
+ * The cursor used to retrieve info on the current track and run the
+ * necessary queries to play audio files
+ */
+ private Cursor mCursor;
+
+ /**
+ * The cursor used to retrieve info on the album the current track is
+ * part of, if any.
+ */
+ private Cursor mAlbumCursor;
+
+ /**
+ * Monitors the audio state
+ */
+ private AudioManager mAudioManager;
+
+ /**
+ * Settings used to save and retrieve the queue and history
+ */
+ private SharedPreferences mPreferences;
+
+ /**
+ * Used to know when the service is active
+ */
+ private boolean mServiceInUse = false;
+
+ /**
+ * Used to know if something should be playing or not
+ */
+ private boolean mIsSupposedToBePlaying = false;
+
+ /**
+ * Used to indicate if the queue can be saved
+ */
+ private boolean mQueueIsSaveable = true;
+
+ /**
+ * Used to track what type of audio focus loss caused the playback to pause
+ */
+ private boolean mPausedByTransientLossOfFocus = false;
+
+ /**
+ * Used to track whether any of Apollo's activities is in the foreground
+ */
+ private boolean mAnyActivityInForeground = false;
+
+ /**
+ * Lock screen controls
+ */
+ private RemoteControlClient mRemoteControlClient;
+
+ private ComponentName mMediaButtonReceiverComponent;
+
+ // We use this to distinguish between different cards when saving/restoring
+ // playlists
+ private int mCardId;
+
+ private int mPlayListLen = 0;
+
+ private int mPlayPos = -1;
+
+ private int mNextPlayPos = -1;
+
+ private int mOpenFailedCounter = 0;
+
+ private int mMediaMountedCount = 0;
+
+ private int mShuffleMode = SHUFFLE_NONE;
+
+ private int mRepeatMode = REPEAT_NONE;
+
+ private int mServiceStartId = -1;
+
+ private long[] mPlayList = null;
+
+ private long[] mAutoShuffleList = null;
+
+ private MusicPlayerHandler mPlayerHandler;
+
+ private BroadcastReceiver mUnmountReceiver = null;
+
+ /**
+ * Image cache
+ */
+ private ImageFetcher mImageFetcher;
+
+ /**
+ * Used to build the notification
+ */
+ private NotificationHelper mNotificationHelper;
+
+ /**
+ * Recently listened database
+ */
+ private RecentStore mRecentsCache;
+
+ /**
+ * Favorites database
+ */
+ private FavoritesStore mFavoritesCache;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public IBinder onBind(final Intent intent) {
+ if (D) Log.d(TAG, "Service bound, intent = " + intent);
+ cancelShutdown();
+ mServiceInUse = true;
+ return mBinder;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onUnbind(final Intent intent) {
+ if (D) Log.d(TAG, "Service unbound");
+ mServiceInUse = false;
+ saveQueue(true);
+
+ if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
+ // Something is currently playing, or will be playing once
+ // an in-progress action requesting audio focus ends, so don't stop
+ // the service now.
+ return true;
+
+ // If there is a playlist but playback is paused, then wait a while
+ // before stopping the service, so that pause/resume isn't slow.
+ // Also delay stopping the service if we're transitioning between
+ // tracks.
+ } else if (mPlayListLen > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
+ scheduleDelayedShutdown();
+ return true;
+ }
+ stopSelf(mServiceStartId);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRebind(final Intent intent) {
+ cancelShutdown();
+ mServiceInUse = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ if (D) Log.d(TAG, "Creating service");
+ super.onCreate();
+
+ // Initialize the favorites and recents databases
+ mRecentsCache = RecentStore.getInstance(this);
+ mFavoritesCache = FavoritesStore.getInstance(this);
+
+ // Initialize the notification helper
+ mNotificationHelper = new NotificationHelper(this);
+
+ // Initialize the image fetcher
+ mImageFetcher = ImageFetcher.getInstance(this);
+ // Initialize the image cache
+ mImageFetcher.setImageCache(ImageCache.getInstance(this));
+
+ // Start up the thread running the service. Note that we create a
+ // separate thread because the service normally runs in the process's
+ // main thread, which we don't want to block. We also make it
+ // background priority so CPU-intensive work will not disrupt the UI.
+ final HandlerThread thread = new HandlerThread("MusicPlayerHandler",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+
+ // Initialize the handler
+ mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
+
+ // Initialize the audio manager and register any headset controls for
+ // playback
+ mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+ mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
+ MediaButtonIntentReceiver.class.getName());
+ mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
+
+ // Use the remote control APIs to set the playback state
+ setUpRemoteControlClient();
+
+ // Initialize the preferences
+ mPreferences = getSharedPreferences("Service", 0);
+ mCardId = getCardId();
+
+ registerExternalStorageListener();
+
+ // Initialize the media player
+ mPlayer = new MultiPlayer(this);
+ mPlayer.setHandler(mPlayerHandler);
+
+ // Initialize the intent filter and each action
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(SERVICECMD);
+ filter.addAction(TOGGLEPAUSE_ACTION);
+ filter.addAction(PAUSE_ACTION);
+ filter.addAction(STOP_ACTION);
+ filter.addAction(NEXT_ACTION);
+ filter.addAction(PREVIOUS_ACTION);
+ filter.addAction(REPEAT_ACTION);
+ filter.addAction(SHUFFLE_ACTION);
+ // Attach the broadcast listener
+ registerReceiver(mIntentReceiver, filter);
+
+ // Initialize the wake lock
+ final PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
+ mWakeLock.setReferenceCounted(false);
+
+ // Initialize the delayed shutdown intent
+ final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
+ shutdownIntent.setAction(SHUTDOWN);
+
+ mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
+
+ // Listen for the idle state
+ scheduleDelayedShutdown();
+
+ // Bring the queue back
+ reloadQueue();
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+ }
+
+ /**
+ * Initializes the remote control client
+ */
+ private void setUpRemoteControlClient() {
+ final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mMediaButtonReceiverComponent);
+ mRemoteControlClient = new RemoteControlClient(
+ PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT));
+ mAudioManager.registerRemoteControlClient(mRemoteControlClient);
+
+ // Flags for the media transport control that this client supports.
+ int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
+ | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
+ | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
+ | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
+
+ if (ApolloUtils.hasJellyBeanMR2()) {
+ flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
+
+ mRemoteControlClient.setOnGetPlaybackPositionListener(
+ new RemoteControlClient.OnGetPlaybackPositionListener() {
+ @Override
+ public long onGetPlaybackPosition() {
+ return position();
+ }
+ });
+ mRemoteControlClient.setPlaybackPositionUpdateListener(
+ new RemoteControlClient.OnPlaybackPositionUpdateListener() {
+ @Override
+ public void onPlaybackPositionUpdate(long newPositionMs) {
+ seek(newPositionMs);
+ }
+ });
+ }
+
+ mRemoteControlClient.setTransportControlFlags(flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroy() {
+ if (D) Log.d(TAG, "Destroying service");
+ super.onDestroy();
+ // Remove any sound effects
+ final Intent audioEffectsIntent = new Intent(
+ AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+ audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
+ audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(audioEffectsIntent);
+
+ // remove any pending alarms
+ mAlarmManager.cancel(mShutdownIntent);
+
+ // Release the player
+ mPlayer.release();
+ mPlayer = null;
+
+ // Remove the audio focus listener and lock screen controls
+ mAudioManager.abandonAudioFocus(mAudioFocusListener);
+ mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
+
+ // Remove any callbacks from the handler
+ mPlayerHandler.removeCallbacksAndMessages(null);
+
+ // Close the cursor
+ closeCursor();
+
+ // Unregister the mount listener
+ unregisterReceiver(mIntentReceiver);
+ if (mUnmountReceiver != null) {
+ unregisterReceiver(mUnmountReceiver);
+ mUnmountReceiver = null;
+ }
+
+ // Release the wake lock
+ mWakeLock.release();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
+ mServiceStartId = startId;
+
+ if (intent != null) {
+ final String action = intent.getAction();
+
+ if (intent.hasExtra(NOW_IN_FOREGROUND)) {
+ mAnyActivityInForeground = intent.getBooleanExtra(NOW_IN_FOREGROUND, false);
+ updateNotification();
+ }
+
+ if (SHUTDOWN.equals(action)) {
+ mShutdownScheduled = false;
+ releaseServiceUiAndStop();
+ return START_NOT_STICKY;
+ }
+
+ handleCommandIntent(intent);
+ }
+
+ // Make sure the service will shut down on its own if it was
+ // just started but not bound to and nothing is playing
+ scheduleDelayedShutdown();
+
+ if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
+ MediaButtonIntentReceiver.completeWakefulIntent(intent);
+ }
+
+ return START_STICKY;
+ }
+
+ private void releaseServiceUiAndStop() {
+ if (isPlaying()
+ || mPausedByTransientLossOfFocus
+ || mPlayerHandler.hasMessages(TRACK_ENDED)) {
+ return;
+ }
+
+ if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
+ mNotificationHelper.killNotification();
+ mAudioManager.abandonAudioFocus(mAudioFocusListener);
+
+ if (!mServiceInUse) {
+ saveQueue(true);
+ stopSelf(mServiceStartId);
+ }
+ }
+
+ private void handleCommandIntent(Intent intent) {
+ final String action = intent.getAction();
+ final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
+
+ if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
+
+ if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
+ gotoNext(true);
+ } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
+ if (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD) {
+ prev();
+ } else {
+ seek(0);
+ play();
+ }
+ } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
+ if (isPlaying()) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else {
+ play();
+ }
+ } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else if (CMDPLAY.equals(command)) {
+ play();
+ } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ seek(0);
+ releaseServiceUiAndStop();
+ } else if (REPEAT_ACTION.equals(action)) {
+ cycleRepeat();
+ } else if (SHUFFLE_ACTION.equals(action)) {
+ cycleShuffle();
+ }
+ }
+
+ /**
+ * Updates the notification, considering the current play and activity state
+ */
+ private void updateNotification() {
+ if (!mAnyActivityInForeground && isPlaying()) {
+ mNotificationHelper.buildNotification(getAlbumName(), getArtistName(),
+ getTrackName(), getAlbumId(), getAlbumArt(), isPlaying());
+ } else if (mAnyActivityInForeground) {
+ mNotificationHelper.killNotification();
+ }
+ }
+
+ /**
+ * @return A card ID used to save and restore playlists, i.e., the queue.
+ */
+ private int getCardId() {
+ final ContentResolver resolver = getContentResolver();
+ Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
+ null, null);
+ int mCardId = -1;
+ if (cursor != null && cursor.moveToFirst()) {
+ mCardId = cursor.getInt(0);
+ cursor.close();
+ cursor = null;
+ }
+ return mCardId;
+ }
+
+ /**
+ * Called when we receive a ACTION_MEDIA_EJECT notification.
+ *
+ * @param storagePath The path to mount point for the removed media
+ */
+ public void closeExternalStorageFiles(final String storagePath) {
+ stop(true);
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+ }
+
+ /**
+ * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
+ * intent will call closeExternalStorageFiles() if the external media is
+ * going to be ejected, so applications can clean up any files they have
+ * open.
+ */
+ public void registerExternalStorageListener() {
+ if (mUnmountReceiver == null) {
+ mUnmountReceiver = new BroadcastReceiver() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ saveQueue(true);
+ mQueueIsSaveable = false;
+ closeExternalStorageFiles(intent.getData().getPath());
+ } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ mMediaMountedCount++;
+ mCardId = getCardId();
+ reloadQueue();
+ mQueueIsSaveable = true;
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+ }
+ }
+ };
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_EJECT);
+ filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ filter.addDataScheme("file");
+ registerReceiver(mUnmountReceiver, filter);
+ }
+ }
+
+ private void scheduleDelayedShutdown() {
+ if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
+ mShutdownScheduled = true;
+ }
+
+ private void cancelShutdown() {
+ if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
+ if (mShutdownScheduled) {
+ mAlarmManager.cancel(mShutdownIntent);
+ mShutdownScheduled = false;
+ }
+ }
+
+ /**
+ * Stops playback
+ *
+ * @param goToIdle True to go to the idle state, false otherwise
+ */
+ private void stop(final boolean goToIdle) {
+ if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
+ if (mPlayer.isInitialized()) {
+ mPlayer.stop();
+ }
+ mFileToPlay = null;
+ closeCursor();
+ if (goToIdle) {
+ scheduleDelayedShutdown();
+ mIsSupposedToBePlaying = false;
+ } else {
+ stopForeground(false);
+ }
+ }
+
+ /**
+ * Removes the range of tracks specified from the play list. If a file
+ * within the range is the file currently being played, playback will move
+ * to the next file after the range.
+ *
+ * @param first The first file to be removed
+ * @param last The last file to be removed
+ * @return the number of tracks deleted
+ */
+ private int removeTracksInternal(int first, int last) {
+ synchronized (this) {
+ if (last < first) {
+ return 0;
+ } else if (first < 0) {
+ first = 0;
+ } else if (last >= mPlayListLen) {
+ last = mPlayListLen - 1;
+ }
+
+ boolean gotonext = false;
+ if (first <= mPlayPos && mPlayPos <= last) {
+ mPlayPos = first;
+ gotonext = true;
+ } else if (mPlayPos > last) {
+ mPlayPos -= last - first + 1;
+ }
+ final int num = mPlayListLen - last - 1;
+ for (int i = 0; i < num; i++) {
+ mPlayList[first + i] = mPlayList[last + 1 + i];
+ }
+ mPlayListLen -= last - first + 1;
+
+ if (gotonext) {
+ if (mPlayListLen == 0) {
+ stop(true);
+ mPlayPos = -1;
+ closeCursor();
+ } else {
+ if (mShuffleMode != SHUFFLE_NONE) {
+ mPlayPos = getNextPosition(true);
+ } else if (mPlayPos >= mPlayListLen) {
+ mPlayPos = 0;
+ }
+ final boolean wasPlaying = isPlaying();
+ stop(false);
+ openCurrentAndNext();
+ if (wasPlaying) {
+ play();
+ }
+ }
+ notifyChange(META_CHANGED);
+ }
+ return last - first + 1;
+ }
+ }
+
+ /**
+ * Adds a list to the playlist
+ *
+ * @param list The list to add
+ * @param position The position to place the tracks
+ */
+ private void addToPlayList(final long[] list, int position) {
+ final int addlen = list.length;
+ if (position < 0) {
+ mPlayListLen = 0;
+ position = 0;
+ }
+ ensurePlayListCapacity(mPlayListLen + addlen);
+ if (position > mPlayListLen) {
+ position = mPlayListLen;
+ }
+
+ final int tailsize = mPlayListLen - position;
+ for (int i = tailsize; i > 0; i--) {
+ mPlayList[position + i] = mPlayList[position + i - addlen];
+ }
+
+ for (int i = 0; i < addlen; i++) {
+ mPlayList[position + i] = list[i];
+ }
+ mPlayListLen += addlen;
+ if (mPlayListLen == 0) {
+ closeCursor();
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * @param trackId The track ID
+ */
+ private void updateCursor(final long trackId) {
+ updateCursor("_id=" + trackId, null);
+ }
+
+ private void updateCursor(final String selection, final String[] selectionArgs) {
+ synchronized (this) {
+ closeCursor();
+ mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ PROJECTION, selection, selectionArgs);
+ }
+ updateAlbumCursor();
+ }
+
+ private void updateCursor(final Uri uri) {
+ synchronized (this) {
+ closeCursor();
+ mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
+ }
+ updateAlbumCursor();
+ }
+
+ private void updateAlbumCursor() {
+ long albumId = getAlbumId();
+ if (albumId >= 0) {
+ mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+ ALBUM_PROJECTION, "_id=" + albumId, null);
+ } else {
+ mAlbumCursor = null;
+ }
+ }
+
+ private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
+ String selection, String[] selectionArgs) {
+ Cursor c = getContentResolver().query(uri, projection,
+ selection, selectionArgs, null, null);
+ if (c == null) {
+ return null;
+ }
+ if (!c.moveToFirst()) {
+ c.close();
+ return null;
+ }
+ return c;
+ }
+
+ private void closeCursor() {
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ if (mAlbumCursor != null) {
+ mAlbumCursor.close();
+ mAlbumCursor = null;
+ }
+ }
+
+ /**
+ * Called to open a new file as the current track and prepare the next for
+ * playback
+ */
+ private void openCurrentAndNext() {
+ openCurrentAndMaybeNext(true);
+ }
+
+ /**
+ * Called to open a new file as the current track and prepare the next for
+ * playback
+ *
+ * @param openNext True to prepare the next track for playback, false
+ * otherwise.
+ */
+ private void openCurrentAndMaybeNext(final boolean openNext) {
+ synchronized (this) {
+ closeCursor();
+
+ if (mPlayListLen == 0) {
+ return;
+ }
+ stop(false);
+
+ updateCursor(mPlayList[mPlayPos]);
+ while (true) {
+ if (mCursor != null
+ && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
+ + mCursor.getLong(IDCOLIDX))) {
+ break;
+ }
+ // if we get here then opening the file failed. We can close the
+ // cursor now, because
+ // we're either going to create a new one next, or stop trying
+ closeCursor();
+ if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
+ final int pos = getNextPosition(false);
+ if (pos < 0) {
+ scheduleDelayedShutdown();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ mPlayPos = pos;
+ stop(false);
+ mPlayPos = pos;
+ updateCursor(mPlayList[mPlayPos]);
+ } else {
+ mOpenFailedCounter = 0;
+ Log.w(TAG, "Failed to open file for playback");
+ scheduleDelayedShutdown();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ }
+ if (openNext) {
+ setNextTrack();
+ }
+ }
+ }
+
+ /**
+ * @param force True to force the player onto the track next, false
+ * otherwise.
+ * @return The next position to play.
+ */
+ private int getNextPosition(final boolean force) {
+ if (!force && mRepeatMode == REPEAT_CURRENT) {
+ if (mPlayPos < 0) {
+ return 0;
+ }
+ return mPlayPos;
+ } else if (mShuffleMode == SHUFFLE_NORMAL) {
+ if (mPlayPos >= 0) {
+ mHistory.add(mPlayPos);
+ }
+ if (mHistory.size() > MAX_HISTORY_SIZE) {
+ mHistory.remove(0);
+ }
+ final int numTracks = mPlayListLen;
+ final int[] tracks = new int[numTracks];
+ for (int i = 0; i < numTracks; i++) {
+ tracks[i] = i;
+ }
+
+ final int numHistory = mHistory.size();
+ int numUnplayed = numTracks;
+ for (int i = 0; i < numHistory; i++) {
+ final int idx = mHistory.get(i).intValue();
+ if (idx < numTracks && tracks[idx] >= 0) {
+ numUnplayed--;
+ tracks[idx] = -1;
+ }
+ }
+ if (numUnplayed <= 0) {
+ if (mRepeatMode == REPEAT_ALL || force) {
+ numUnplayed = numTracks;
+ for (int i = 0; i < numTracks; i++) {
+ tracks[i] = i;
+ }
+ } else {
+ return -1;
+ }
+ }
+ int skip = 0;
+ if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
+ skip = mShuffler.nextInt(numUnplayed);
+ }
+ int cnt = -1;
+ while (true) {
+ while (tracks[++cnt] < 0) {
+ ;
+ }
+ skip--;
+ if (skip < 0) {
+ break;
+ }
+ }
+ return cnt;
+ } else if (mShuffleMode == SHUFFLE_AUTO) {
+ doAutoShuffleUpdate();
+ return mPlayPos + 1;
+ } else {
+ if (mPlayPos >= mPlayListLen - 1) {
+ if (mRepeatMode == REPEAT_NONE && !force) {
+ return -1;
+ } else if (mRepeatMode == REPEAT_ALL || force) {
+ return 0;
+ }
+ return -1;
+ } else {
+ return mPlayPos + 1;
+ }
+ }
+ }
+
+ /**
+ * Sets the track track to be played
+ */
+ private void setNextTrack() {
+ mNextPlayPos = getNextPosition(false);
+ if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
+ if (mNextPlayPos >= 0 && mPlayList != null) {
+ final long id = mPlayList[mNextPlayPos];
+ mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
+ } else {
+ mPlayer.setNextDataSource(null);
+ }
+ }
+
+ /**
+ * Creates a shuffled playlist used for party mode
+ */
+ private boolean makeAutoShuffleList() {
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ MediaStore.Audio.Media._ID
+ }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
+ if (cursor == null || cursor.getCount() == 0) {
+ return false;
+ }
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
+ for (int i = 0; i < len; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getLong(0);
+ }
+ mAutoShuffleList = list;
+ return true;
+ } catch (final RuntimeException e) {
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates the party shuffle playlist
+ */
+ private void doAutoShuffleUpdate() {
+ boolean notify = false;
+ if (mPlayPos > 10) {
+ removeTracks(0, mPlayPos - 9);
+ notify = true;
+ }
+ final int toAdd = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
+ for (int i = 0; i < toAdd; i++) {
+ int lookback = mHistory.size();
+ int idx = -1;
+ while (true) {
+ idx = mShuffler.nextInt(mAutoShuffleList.length);
+ if (!wasRecentlyUsed(idx, lookback)) {
+ break;
+ }
+ lookback /= 2;
+ }
+ mHistory.add(idx);
+ if (mHistory.size() > MAX_HISTORY_SIZE) {
+ mHistory.remove(0);
+ }
+ ensurePlayListCapacity(mPlayListLen + 1);
+ mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
+ notify = true;
+ }
+ if (notify) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ }
+
+ /**/
+ private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
+ if (lookbacksize == 0) {
+ return false;
+ }
+ final int histsize = mHistory.size();
+ if (histsize < lookbacksize) {
+ lookbacksize = histsize;
+ }
+ final int maxidx = histsize - 1;
+ for (int i = 0; i < lookbacksize; i++) {
+ final long entry = mHistory.get(maxidx - i);
+ if (entry == idx) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Makes sure the playlist has enough space to hold all of the songs
+ *
+ * @param size The size of the playlist
+ */
+ private void ensurePlayListCapacity(final int size) {
+ if (mPlayList == null || size > mPlayList.length) {
+ // reallocate at 2x requested size so we don't
+ // need to grow and copy the array for every
+ // insert
+ final long[] newlist = new long[size * 2];
+ final int len = mPlayList != null ? mPlayList.length : mPlayListLen;
+ for (int i = 0; i < len; i++) {
+ newlist[i] = mPlayList[i];
+ }
+ mPlayList = newlist;
+ }
+ // FIXME: shrink the array when the needed size is much smaller
+ // than the allocated size
+ }
+
+ /**
+ * Notify the change-receivers that something has changed.
+ */
+ private void notifyChange(final String what) {
+ if (D) Log.d(TAG, "notifyChange: what = " + what);
+
+ // Update the lockscreen controls
+ updateRemoteControlClient(what);
+
+ if (what.equals(POSITION_CHANGED)) {
+ return;
+ }
+
+ final Intent intent = new Intent(what);
+ intent.putExtra("id", getAudioId());
+ intent.putExtra("artist", getArtistName());
+ intent.putExtra("album", getAlbumName());
+ intent.putExtra("track", getTrackName());
+ intent.putExtra("playing", isPlaying());
+ intent.putExtra("isfavorite", isFavorite());
+ sendStickyBroadcast(intent);
+
+ final Intent musicIntent = new Intent(intent);
+ musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
+ sendStickyBroadcast(musicIntent);
+
+ if (what.equals(META_CHANGED)) {
+ // Increase the play count for favorite songs.
+ if (mFavoritesCache.getSongId(getAudioId()) != null) {
+ mFavoritesCache.addSongId(getAudioId(), getTrackName(), getAlbumName(),
+ getArtistName());
+ }
+ // Add the track to the recently played list.
+ mRecentsCache.addAlbumId(getAlbumId(), getAlbumName(), getArtistName(),
+ MusicUtils.getSongCountForAlbum(this, getAlbumId()),
+ MusicUtils.getReleaseDateForAlbum(this, getAlbumId()));
+ } else if (what.equals(QUEUE_CHANGED)) {
+ saveQueue(true);
+ if (isPlaying()) {
+ setNextTrack();
+ }
+ } else {
+ saveQueue(false);
+ }
+
+ if (what.equals(PLAYSTATE_CHANGED)) {
+ mNotificationHelper.updatePlayState(isPlaying());
+ }
+
+ // Update the app-widgets
+ mAppWidgetSmall.notifyChange(this, what);
+ mAppWidgetLarge.notifyChange(this, what);
+ mAppWidgetLargeAlternate.notifyChange(this, what);
+ mRecentWidgetProvider.notifyChange(this, what);
+ }
+
+ /**
+ * Updates the lockscreen controls.
+ *
+ * @param what The broadcast
+ */
+ private void updateRemoteControlClient(final String what) {
+ int playState = mIsSupposedToBePlaying
+ ? RemoteControlClient.PLAYSTATE_PLAYING
+ : RemoteControlClient.PLAYSTATE_PAUSED;
+
+ if (ApolloUtils.hasJellyBeanMR2()
+ && (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED))) {
+ mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
+ } else if (what.equals(PLAYSTATE_CHANGED)) {
+ mRemoteControlClient.setPlaybackState(playState);
+ } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
+ Bitmap albumArt = getAlbumArt();
+ if (albumArt != null) {
+ // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
+ // to make sure not to hand out our cache copy
+ Bitmap.Config config = albumArt.getConfig();
+ if (config == null) {
+ config = Bitmap.Config.ARGB_8888;
+ }
+ albumArt = albumArt.copy(config, false);
+ }
+ mRemoteControlClient
+ .editMetadata(true)
+ .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
+ getAlbumArtistName())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName())
+ .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName())
+ .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration())
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, albumArt)
+ .apply();
+
+ if (ApolloUtils.hasJellyBeanMR2()) {
+ mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
+ }
+ }
+ }
+
+ /**
+ * Saves the queue
+ *
+ * @param full True if the queue is full
+ */
+ private void saveQueue(final boolean full) {
+ if (!mQueueIsSaveable) {
+ return;
+ }
+
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ if (full) {
+ final StringBuilder q = new StringBuilder();
+ int len = mPlayListLen;
+ for (int i = 0; i < len; i++) {
+ long n = mPlayList[i];
+ if (n < 0) {
+ continue;
+ } else if (n == 0) {
+ q.append("0;");
+ } else {
+ while (n != 0) {
+ final int digit = (int)(n & 0xf);
+ n >>>= 4;
+ q.append(HEX_DIGITS[digit]);
+ }
+ q.append(";");
+ }
+ }
+ editor.putString("queue", q.toString());
+ editor.putInt("cardid", mCardId);
+ if (mShuffleMode != SHUFFLE_NONE) {
+ len = mHistory.size();
+ q.setLength(0);
+ for (int i = 0; i < len; i++) {
+ int n = mHistory.get(i);
+ if (n == 0) {
+ q.append("0;");
+ } else {
+ while (n != 0) {
+ final int digit = n & 0xf;
+ n >>>= 4;
+ q.append(HEX_DIGITS[digit]);
+ }
+ q.append(";");
+ }
+ }
+ editor.putString("history", q.toString());
+ }
+ }
+ editor.putInt("curpos", mPlayPos);
+ if (mPlayer.isInitialized()) {
+ editor.putLong("seekpos", mPlayer.position());
+ }
+ editor.putInt("repeatmode", mRepeatMode);
+ editor.putInt("shufflemode", mShuffleMode);
+ editor.apply();
+ }
+
+ /**
+ * Reloads the queue as the user left it the last time they stopped using
+ * Apollo
+ */
+ private void reloadQueue() {
+ String q = null;
+ int id = mCardId;
+ if (mPreferences.contains("cardid")) {
+ id = mPreferences.getInt("cardid", ~mCardId);
+ }
+ if (id == mCardId) {
+ q = mPreferences.getString("queue", "");
+ }
+ int qlen = q != null ? q.length() : 0;
+ if (qlen > 1) {
+ int plen = 0;
+ int n = 0;
+ int shift = 0;
+ for (int i = 0; i < qlen; i++) {
+ final char c = q.charAt(i);
+ if (c == ';') {
+ ensurePlayListCapacity(plen + 1);
+ mPlayList[plen] = n;
+ plen++;
+ n = 0;
+ shift = 0;
+ } else {
+ if (c >= '0' && c <= '9') {
+ n += c - '0' << shift;
+ } else if (c >= 'a' && c <= 'f') {
+ n += 10 + c - 'a' << shift;
+ } else {
+ plen = 0;
+ break;
+ }
+ shift += 4;
+ }
+ }
+ mPlayListLen = plen;
+ final int pos = mPreferences.getInt("curpos", 0);
+ if (pos < 0 || pos >= mPlayListLen) {
+ mPlayListLen = 0;
+ return;
+ }
+ mPlayPos = pos;
+ updateCursor(mPlayList[mPlayPos]);
+ if (mCursor == null) {
+ SystemClock.sleep(3000);
+ updateCursor(mPlayList[mPlayPos]);
+ }
+ synchronized (this) {
+ closeCursor();
+ mOpenFailedCounter = 20;
+ openCurrentAndNext();
+ }
+ if (!mPlayer.isInitialized()) {
+ mPlayListLen = 0;
+ return;
+ }
+
+ final long seekpos = mPreferences.getLong("seekpos", 0);
+ seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
+
+ if (D) {
+ Log.d(TAG, "restored queue, currently at position "
+ + position() + "/" + duration()
+ + " (requested " + seekpos + ")");
+ }
+
+ int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
+ if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
+ repmode = REPEAT_NONE;
+ }
+ mRepeatMode = repmode;
+
+ int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
+ if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
+ shufmode = SHUFFLE_NONE;
+ }
+ if (shufmode != SHUFFLE_NONE) {
+ q = mPreferences.getString("history", "");
+ qlen = q != null ? q.length() : 0;
+ if (qlen > 1) {
+ plen = 0;
+ n = 0;
+ shift = 0;
+ mHistory.clear();
+ for (int i = 0; i < qlen; i++) {
+ final char c = q.charAt(i);
+ if (c == ';') {
+ if (n >= mPlayListLen) {
+ mHistory.clear();
+ break;
+ }
+ mHistory.add(n);
+ n = 0;
+ shift = 0;
+ } else {
+ if (c >= '0' && c <= '9') {
+ n += c - '0' << shift;
+ } else if (c >= 'a' && c <= 'f') {
+ n += 10 + c - 'a' << shift;
+ } else {
+ mHistory.clear();
+ break;
+ }
+ shift += 4;
+ }
+ }
+ }
+ }
+ if (shufmode == SHUFFLE_AUTO) {
+ if (!makeAutoShuffleList()) {
+ shufmode = SHUFFLE_NONE;
+ }
+ }
+ mShuffleMode = shufmode;
+ }
+ }
+
+ /**
+ * Opens a file and prepares it for playback
+ *
+ * @param path The path of the file to open
+ */
+ public boolean openFile(final String path) {
+ if (D) Log.d(TAG, "openFile: path = " + path);
+ synchronized (this) {
+ if (path == null) {
+ return false;
+ }
+
+ // If mCursor is null, try to associate path with a database cursor
+ if (mCursor == null) {
+ Uri uri = Uri.parse(path);
+ long id = -1;
+ try {
+ id = Long.valueOf(uri.getLastPathSegment());
+ } catch (NumberFormatException ex) {
+ // Ignore
+ }
+
+ if (id != -1 && path.startsWith(MediaStore.Audio.Media.
+ EXTERNAL_CONTENT_URI.toString())) {
+ updateCursor(uri);
+
+ } else if (id != -1 && path.startsWith(MediaStore.Files.getContentUri(
+ "external").toString())) {
+ updateCursor(id);
+
+ } else {
+ String where = MediaStore.Audio.Media.DATA + "=?";
+ String[] selectionArgs = new String[] {path};
+ updateCursor(where, selectionArgs);
+ }
+ try {
+ if (mCursor != null) {
+ ensurePlayListCapacity(1);
+ mPlayListLen = 1;
+ mPlayList[0] = mCursor.getLong(IDCOLIDX);
+ mPlayPos = 0;
+ }
+ } catch (final UnsupportedOperationException ex) {
+ }
+ }
+ mFileToPlay = path;
+ mPlayer.setDataSource(mFileToPlay);
+ if (mPlayer.isInitialized()) {
+ mOpenFailedCounter = 0;
+ return true;
+ }
+ stop(true);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the audio session ID
+ *
+ * @return The current media player audio session ID
+ */
+ public int getAudioSessionId() {
+ synchronized (this) {
+ return mPlayer.getAudioSessionId();
+ }
+ }
+
+ /**
+ * Indicates if the media storeage device has been mounted or not
+ *
+ * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
+ */
+ public int getMediaMountedCount() {
+ return mMediaMountedCount;
+ }
+
+ /**
+ * Returns the shuffle mode
+ *
+ * @return The current shuffle mode (all, party, none)
+ */
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ /**
+ * Returns the repeat mode
+ *
+ * @return The current repeat mode (all, one, none)
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Removes all instances of the track with the given ID from the playlist.
+ *
+ * @param id The id to be removed
+ * @return how many instances of the track were removed
+ */
+ public int removeTrack(final long id) {
+ int numremoved = 0;
+ synchronized (this) {
+ for (int i = 0; i < mPlayListLen; i++) {
+ if (mPlayList[i] == id) {
+ numremoved += removeTracksInternal(i, i);
+ i--;
+ }
+ }
+ }
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ /**
+ * Removes the range of tracks specified from the play list. If a file
+ * within the range is the file currently being played, playback will move
+ * to the next file after the range.
+ *
+ * @param first The first file to be removed
+ * @param last The last file to be removed
+ * @return the number of tracks deleted
+ */
+ public int removeTracks(final int first, final int last) {
+ final int numremoved = removeTracksInternal(first, last);
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ /**
+ * Returns the position in the queue
+ *
+ * @return the current position in the queue
+ */
+ public int getQueuePosition() {
+ synchronized (this) {
+ return mPlayPos;
+ }
+ }
+
+ /**
+ * Returns the path to current song
+ *
+ * @return The path to the current song
+ */
+ public String getPath() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
+ }
+ }
+
+ /**
+ * Returns the album name
+ *
+ * @return The current song album Name
+ */
+ public String getAlbumName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
+ }
+ }
+
+ /**
+ * Returns the song name
+ *
+ * @return The current song name
+ */
+ public String getTrackName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
+ }
+ }
+
+ /**
+ * Returns the artist name
+ *
+ * @return The current song artist name
+ */
+ public String getArtistName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
+ }
+ }
+
+ /**
+ * Returns the artist name
+ *
+ * @return The current song artist name
+ */
+ public String getAlbumArtistName() {
+ synchronized (this) {
+ if (mAlbumCursor == null) {
+ return null;
+ }
+ return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
+ }
+ }
+
+ /**
+ * Returns the album ID
+ *
+ * @return The current song album ID
+ */
+ public long getAlbumId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
+ }
+ }
+
+ /**
+ * Returns the artist ID
+ *
+ * @return The current song artist ID
+ */
+ public long getArtistId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
+ }
+ }
+
+ /**
+ * Returns the current audio ID
+ *
+ * @return The current track ID
+ */
+ public long getAudioId() {
+ synchronized (this) {
+ if (mPlayPos >= 0 && mPlayer.isInitialized()) {
+ return mPlayList[mPlayPos];
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Seeks the current track to a specific time
+ *
+ * @param position The time to seek to
+ * @return The time to play the track at
+ */
+ public long seek(long position) {
+ if (mPlayer.isInitialized()) {
+ if (position < 0) {
+ position = 0;
+ } else if (position > mPlayer.duration()) {
+ position = mPlayer.duration();
+ }
+ long result = mPlayer.seek(position);
+ notifyChange(POSITION_CHANGED);
+ return result;
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the current position in time of the currenttrack
+ *
+ * @return The current playback position in miliseconds
+ */
+ public long position() {
+ if (mPlayer.isInitialized()) {
+ return mPlayer.position();
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the full duration of the current track
+ *
+ * @return The duration of the current track in miliseconds
+ */
+ public long duration() {
+ if (mPlayer.isInitialized()) {
+ return mPlayer.duration();
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the queue
+ *
+ * @return The queue as a long[]
+ */
+ public long[] getQueue() {
+ synchronized (this) {
+ final int len = mPlayListLen;
+ final long[] list = new long[len];
+ for (int i = 0; i < len; i++) {
+ list[i] = mPlayList[i];
+ }
+ return list;
+ }
+ }
+
+ /**
+ * @return True if music is playing, false otherwise
+ */
+ public boolean isPlaying() {
+ return mIsSupposedToBePlaying;
+ }
+
+ /**
+ * True if the current track is a "favorite", false otherwise
+ */
+ public boolean isFavorite() {
+ if (mFavoritesCache != null) {
+ synchronized (this) {
+ final Long id = mFavoritesCache.getSongId(getAudioId());
+ return id != null ? true : false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Opens a list for playback
+ *
+ * @param list The list of tracks to open
+ * @param position The position to start playback at
+ */
+ public void open(final long[] list, final int position) {
+ synchronized (this) {
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ mShuffleMode = SHUFFLE_NORMAL;
+ }
+ final long oldId = getAudioId();
+ final int listlength = list.length;
+ boolean newlist = true;
+ if (mPlayListLen == listlength) {
+ newlist = false;
+ for (int i = 0; i < listlength; i++) {
+ if (list[i] != mPlayList[i]) {
+ newlist = true;
+ break;
+ }
+ }
+ }
+ if (newlist) {
+ addToPlayList(list, -1);
+ notifyChange(QUEUE_CHANGED);
+ }
+ if (position >= 0) {
+ mPlayPos = position;
+ } else {
+ mPlayPos = mShuffler.nextInt(mPlayListLen);
+ }
+ mHistory.clear();
+ openCurrentAndNext();
+ if (oldId != getAudioId()) {
+ notifyChange(META_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Stops playback.
+ */
+ public void stop() {
+ stop(true);
+ }
+
+ /**
+ * Resumes or starts playback.
+ */
+ public void play() {
+ int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
+ AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+
+ if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
+
+ if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ return;
+ }
+
+ mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
+ MediaButtonIntentReceiver.class.getName()));
+
+ setNextTrack();
+
+ if (mPlayer.isInitialized()) {
+ final long duration = mPlayer.duration();
+ if (mRepeatMode != REPEAT_CURRENT && duration > 2000
+ && mPlayer.position() >= duration - 2000) {
+ gotoNext(true);
+ }
+
+ mPlayer.start();
+ mPlayerHandler.removeMessages(FADEDOWN);
+ mPlayerHandler.sendEmptyMessage(FADEUP);
+
+ if (!mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = true;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+
+ cancelShutdown();
+ updateNotification();
+ } else if (mPlayListLen <= 0) {
+ setShuffleMode(SHUFFLE_AUTO);
+ }
+ }
+
+ /**
+ * Temporarily pauses playback.
+ */
+ public void pause() {
+ if (D) Log.d(TAG, "Pausing playback");
+ synchronized (this) {
+ mPlayerHandler.removeMessages(FADEUP);
+ if (mIsSupposedToBePlaying) {
+ mPlayer.pause();
+ scheduleDelayedShutdown();
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Changes from the current track to the next track
+ */
+ public void gotoNext(final boolean force) {
+ if (D) Log.d(TAG, "Going to next track");
+ synchronized (this) {
+ if (mPlayListLen <= 0) {
+ if (D) Log.d(TAG, "No play queue");
+ scheduleDelayedShutdown();
+ return;
+ }
+ final int pos = getNextPosition(force);
+ if (pos < 0) {
+ scheduleDelayedShutdown();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ mPlayPos = pos;
+ stop(false);
+ mPlayPos = pos;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * Changes from the current track to the previous played track
+ */
+ public void prev() {
+ if (D) Log.d(TAG, "Going to previous track");
+ synchronized (this) {
+ if (mShuffleMode == SHUFFLE_NORMAL) {
+ // Go to previously-played track and remove it from the history
+ final int histsize = mHistory.size();
+ if (histsize == 0) {
+ return;
+ }
+ final Integer pos = mHistory.remove(histsize - 1);
+ mPlayPos = pos.intValue();
+ } else {
+ if (mPlayPos > 0) {
+ mPlayPos--;
+ } else {
+ mPlayPos = mPlayListLen - 1;
+ }
+ }
+ stop(false);
+ openCurrent();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * We don't want to open the current and next track when the user is using
+ * the {@code #prev()} method because they won't be able to travel back to
+ * the previously listened track if they're shuffling.
+ */
+ private void openCurrent() {
+ openCurrentAndMaybeNext(false);
+ }
+
+ /**
+ * Toggles the current song as a favorite.
+ */
+ public void toggleFavorite() {
+ if (mFavoritesCache != null) {
+ synchronized (this) {
+ mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
+ getArtistName());
+ }
+ }
+ }
+
+ /**
+ * Moves an item in the queue from one position to another
+ *
+ * @param from The position the item is currently at
+ * @param to The position the item is being moved to
+ */
+ public void moveQueueItem(int index1, int index2) {
+ synchronized (this) {
+ if (index1 >= mPlayListLen) {
+ index1 = mPlayListLen - 1;
+ }
+ if (index2 >= mPlayListLen) {
+ index2 = mPlayListLen - 1;
+ }
+ if (index1 < index2) {
+ final long tmp = mPlayList[index1];
+ for (int i = index1; i < index2; i++) {
+ mPlayList[i] = mPlayList[i + 1];
+ }
+ mPlayList[index2] = tmp;
+ if (mPlayPos == index1) {
+ mPlayPos = index2;
+ } else if (mPlayPos >= index1 && mPlayPos <= index2) {
+ mPlayPos--;
+ }
+ } else if (index2 < index1) {
+ final long tmp = mPlayList[index1];
+ for (int i = index1; i > index2; i--) {
+ mPlayList[i] = mPlayList[i - 1];
+ }
+ mPlayList[index2] = tmp;
+ if (mPlayPos == index1) {
+ mPlayPos = index2;
+ } else if (mPlayPos >= index2 && mPlayPos <= index1) {
+ mPlayPos++;
+ }
+ }
+ notifyChange(QUEUE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the repeat mode
+ *
+ * @param repeatmode The repeat mode to use
+ */
+ public void setRepeatMode(final int repeatmode) {
+ synchronized (this) {
+ mRepeatMode = repeatmode;
+ setNextTrack();
+ saveQueue(false);
+ notifyChange(REPEATMODE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the shuffle mode
+ *
+ * @param shufflemode The shuffle mode to use
+ */
+ public void setShuffleMode(final int shufflemode) {
+ synchronized (this) {
+ if (mShuffleMode == shufflemode && mPlayListLen > 0) {
+ return;
+ }
+ mShuffleMode = shufflemode;
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ if (makeAutoShuffleList()) {
+ mPlayListLen = 0;
+ doAutoShuffleUpdate();
+ mPlayPos = 0;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ return;
+ } else {
+ mShuffleMode = SHUFFLE_NONE;
+ }
+ }
+ saveQueue(false);
+ notifyChange(SHUFFLEMODE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the position of a track in the queue
+ *
+ * @param index The position to place the track
+ */
+ public void setQueuePosition(final int index) {
+ synchronized (this) {
+ stop(false);
+ mPlayPos = index;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ doAutoShuffleUpdate();
+ }
+ }
+ }
+
+ /**
+ * Queues a new list for playback
+ *
+ * @param list The list to queue
+ * @param action The action to take
+ */
+ public void enqueue(final long[] list, final int action) {
+ synchronized (this) {
+ if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
+ addToPlayList(list, mPlayPos + 1);
+ notifyChange(QUEUE_CHANGED);
+ } else {
+ addToPlayList(list, Integer.MAX_VALUE);
+ notifyChange(QUEUE_CHANGED);
+ if (action == NOW) {
+ mPlayPos = mPlayListLen - list.length;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ return;
+ }
+ }
+ if (mPlayPos < 0) {
+ mPlayPos = 0;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Cycles through the different repeat modes
+ */
+ private void cycleRepeat() {
+ if (mRepeatMode == REPEAT_NONE) {
+ setRepeatMode(REPEAT_ALL);
+ } else if (mRepeatMode == REPEAT_ALL) {
+ setRepeatMode(REPEAT_CURRENT);
+ if (mShuffleMode != SHUFFLE_NONE) {
+ setShuffleMode(SHUFFLE_NONE);
+ }
+ } else {
+ setRepeatMode(REPEAT_NONE);
+ }
+ }
+
+ /**
+ * Cycles through the different shuffle modes
+ */
+ private void cycleShuffle() {
+ if (mShuffleMode == SHUFFLE_NONE) {
+ setShuffleMode(SHUFFLE_NORMAL);
+ if (mRepeatMode == REPEAT_CURRENT) {
+ setRepeatMode(REPEAT_ALL);
+ }
+ } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
+ setShuffleMode(SHUFFLE_NONE);
+ }
+ }
+
+ /**
+ * @return The album art for the current album.
+ */
+ public Bitmap getAlbumArt() {
+ // Return the cached artwork
+ final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
+ getAlbumId(), getArtistName());
+ return bitmap;
+ }
+
+ /**
+ * Called when one of the lists should refresh or requery.
+ */
+ public void refresh() {
+ notifyChange(REFRESH);
+ }
+
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String command = intent.getStringExtra(CMDNAME);
+
+ if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
+ } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
+ } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] largeAlt = intent
+ .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
+ } else if (RecentWidgetProvider.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] recent = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mRecentWidgetProvider.performUpdate(MusicPlaybackService.this, recent);
+ } else {
+ handleCommandIntent(intent);
+ }
+ }
+ };
+
+ private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
+ }
+ };
+
+ private static final class MusicPlayerHandler extends Handler {
+ private final WeakReference<MusicPlaybackService> mService;
+ private float mCurrentVolume = 1.0f;
+
+ /**
+ * Constructor of <code>MusicPlayerHandler</code>
+ *
+ * @param service The service to use.
+ * @param looper The thread to run on.
+ */
+ public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
+ super(looper);
+ mService = new WeakReference<MusicPlaybackService>(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void handleMessage(final Message msg) {
+ final MusicPlaybackService service = mService.get();
+ if (service == null) {
+ return;
+ }
+
+ switch (msg.what) {
+ case FADEDOWN:
+ mCurrentVolume -= .05f;
+ if (mCurrentVolume > .2f) {
+ sendEmptyMessageDelayed(FADEDOWN, 10);
+ } else {
+ mCurrentVolume = .2f;
+ }
+ service.mPlayer.setVolume(mCurrentVolume);
+ break;
+ case FADEUP:
+ mCurrentVolume += .01f;
+ if (mCurrentVolume < 1.0f) {
+ sendEmptyMessageDelayed(FADEUP, 10);
+ } else {
+ mCurrentVolume = 1.0f;
+ }
+ service.mPlayer.setVolume(mCurrentVolume);
+ break;
+ case SERVER_DIED:
+ if (service.isPlaying()) {
+ service.gotoNext(true);
+ } else {
+ service.openCurrentAndNext();
+ }
+ break;
+ case TRACK_WENT_TO_NEXT:
+ service.mPlayPos = service.mNextPlayPos;
+ if (service.mCursor != null) {
+ service.mCursor.close();
+ }
+ service.updateCursor(service.mPlayList[service.mPlayPos]);
+ service.notifyChange(META_CHANGED);
+ service.updateNotification();
+ service.setNextTrack();
+ break;
+ case TRACK_ENDED:
+ if (service.mRepeatMode == REPEAT_CURRENT) {
+ service.seek(0);
+ service.play();
+ } else {
+ service.gotoNext(false);
+ }
+ break;
+ case RELEASE_WAKELOCK:
+ service.mWakeLock.release();
+ break;
+ case FOCUSCHANGE:
+ if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
+ switch (msg.arg1) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (service.isPlaying()) {
+ service.mPausedByTransientLossOfFocus =
+ msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
+ }
+ service.pause();
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ removeMessages(FADEUP);
+ sendEmptyMessage(FADEDOWN);
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (!service.isPlaying()
+ && service.mPausedByTransientLossOfFocus) {
+ service.mPausedByTransientLossOfFocus = false;
+ mCurrentVolume = 0f;
+ service.mPlayer.setVolume(mCurrentVolume);
+ service.play();
+ } else {
+ removeMessages(FADEDOWN);
+ sendEmptyMessage(FADEUP);
+ }
+ break;
+ default:
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private static final class Shuffler {
+
+ private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
+
+ private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
+
+ private final Random mRandom = new Random();
+
+ private int mPrevious;
+
+ /**
+ * Constructor of <code>Shuffler</code>
+ */
+ public Shuffler() {
+ super();
+ }
+
+ /**
+ * @param interval The length the queue
+ * @return The position of the next track to play
+ */
+ public int nextInt(final int interval) {
+ int next;
+ do {
+ next = mRandom.nextInt(interval);
+ } while (next == mPrevious && interval > 1
+ && !mPreviousNumbers.contains(Integer.valueOf(next)));
+ mPrevious = next;
+ mHistoryOfNumbers.add(mPrevious);
+ mPreviousNumbers.add(mPrevious);
+ cleanUpHistory();
+ return next;
+ }
+
+ /**
+ * Removes old tracks and cleans up the history preparing for new tracks
+ * to be added to the mapping
+ */
+ private void cleanUpHistory() {
+ if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
+ for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
+ mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
+ }
+ }
+ }
+ };
+
+ private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
+ MediaPlayer.OnCompletionListener {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
+
+ private MediaPlayer mNextMediaPlayer;
+
+ private Handler mHandler;
+
+ private boolean mIsInitialized = false;
+
+ /**
+ * Constructor of <code>MultiPlayer</code>
+ */
+ public MultiPlayer(final MusicPlaybackService service) {
+ mService = new WeakReference<MusicPlaybackService>(service);
+ mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ }
+
+ /**
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ */
+ public void setDataSource(final String path) {
+ mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
+ if (mIsInitialized) {
+ setNextDataSource(null);
+ }
+ }
+
+ /**
+ * @param player The {@link MediaPlayer} to use
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ * @return True if the <code>player</code> has been prepared and is
+ * ready to play, false otherwise
+ */
+ private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
+ try {
+ player.reset();
+ player.setOnPreparedListener(null);
+ if (path.startsWith("content://")) {
+ player.setDataSource(mService.get(), Uri.parse(path));
+ } else {
+ player.setDataSource(path);
+ }
+ player.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ player.prepare();
+ } catch (final IOException todo) {
+ // TODO: notify the user why the file couldn't be opened
+ return false;
+ } catch (final IllegalArgumentException todo) {
+ // TODO: notify the user why the file couldn't be opened
+ return false;
+ }
+ player.setOnCompletionListener(this);
+ player.setOnErrorListener(this);
+ final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+ intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
+ intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, mService.get().getPackageName());
+ mService.get().sendBroadcast(intent);
+ return true;
+ }
+
+ /**
+ * Set the MediaPlayer to start when this MediaPlayer finishes playback.
+ *
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ */
+ public void setNextDataSource(final String path) {
+ try {
+ mCurrentMediaPlayer.setNextMediaPlayer(null);
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "Next media player is current one, continuing");
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Media player not initialized!");
+ return;
+ }
+ if (mNextMediaPlayer != null) {
+ mNextMediaPlayer.release();
+ mNextMediaPlayer = null;
+ }
+ if (path == null) {
+ return;
+ }
+ mNextMediaPlayer = new MediaPlayer();
+ mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
+ if (setDataSourceImpl(mNextMediaPlayer, path)) {
+ mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
+ } else {
+ if (mNextMediaPlayer != null) {
+ mNextMediaPlayer.release();
+ mNextMediaPlayer = null;
+ }
+ }
+ }
+
+ /**
+ * Sets the handler
+ *
+ * @param handler The handler to use
+ */
+ public void setHandler(final Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * @return True if the player is ready to go, false otherwise
+ */
+ public boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * Starts or resumes playback.
+ */
+ public void start() {
+ mCurrentMediaPlayer.start();
+ }
+
+ /**
+ * Resets the MediaPlayer to its uninitialized state.
+ */
+ public void stop() {
+ mCurrentMediaPlayer.reset();
+ mIsInitialized = false;
+ }
+
+ /**
+ * Releases resources associated with this MediaPlayer object.
+ */
+ public void release() {
+ stop();
+ mCurrentMediaPlayer.release();
+ }
+
+ /**
+ * Pauses playback. Call start() to resume.
+ */
+ public void pause() {
+ mCurrentMediaPlayer.pause();
+ }
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return The duration in milliseconds
+ */
+ public long duration() {
+ return mCurrentMediaPlayer.getDuration();
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return The current position in milliseconds
+ */
+ public long position() {
+ return mCurrentMediaPlayer.getCurrentPosition();
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @param whereto The offset in milliseconds from the start to seek to
+ * @return The offset in milliseconds from the start to seek to
+ */
+ public long seek(final long whereto) {
+ mCurrentMediaPlayer.seekTo((int)whereto);
+ return whereto;
+ }
+
+ /**
+ * Sets the volume on this player.
+ *
+ * @param vol Left and right volume scalar
+ */
+ public void setVolume(final float vol) {
+ mCurrentMediaPlayer.setVolume(vol, vol);
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId The audio session ID
+ */
+ public void setAudioSessionId(final int sessionId) {
+ mCurrentMediaPlayer.setAudioSessionId(sessionId);
+ }
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return The current audio session ID.
+ */
+ public int getAudioSessionId() {
+ return mCurrentMediaPlayer.getAudioSessionId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onError(final MediaPlayer mp, final int what, final int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+ mIsInitialized = false;
+ mCurrentMediaPlayer.release();
+ mCurrentMediaPlayer = new MediaPlayer();
+ mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCompletion(final MediaPlayer mp) {
+ if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
+ mCurrentMediaPlayer.release();
+ mCurrentMediaPlayer = mNextMediaPlayer;
+ mNextMediaPlayer = null;
+ mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
+ } else {
+ mService.get().mWakeLock.acquire(30000);
+ mHandler.sendEmptyMessage(TRACK_ENDED);
+ mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
+ }
+ }
+ }
+
+ private static final class ServiceStub extends IElevenService.Stub {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ private ServiceStub(final MusicPlaybackService service) {
+ mService = new WeakReference<MusicPlaybackService>(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openFile(final String path) throws RemoteException {
+ mService.get().openFile(path);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open(final long[] list, final int position) throws RemoteException {
+ mService.get().open(list, position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stop() throws RemoteException {
+ mService.get().stop();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pause() throws RemoteException {
+ mService.get().pause();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void play() throws RemoteException {
+ mService.get().play();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void prev() throws RemoteException {
+ mService.get().prev();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void next() throws RemoteException {
+ mService.get().gotoNext(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void enqueue(final long[] list, final int action) throws RemoteException {
+ mService.get().enqueue(list, action);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setQueuePosition(final int index) throws RemoteException {
+ mService.get().setQueuePosition(index);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setShuffleMode(final int shufflemode) throws RemoteException {
+ mService.get().setShuffleMode(shufflemode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRepeatMode(final int repeatmode) throws RemoteException {
+ mService.get().setRepeatMode(repeatmode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void moveQueueItem(final int from, final int to) throws RemoteException {
+ mService.get().moveQueueItem(from, to);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void toggleFavorite() throws RemoteException {
+ mService.get().toggleFavorite();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void refresh() throws RemoteException {
+ mService.get().refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isFavorite() throws RemoteException {
+ return mService.get().isFavorite();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isPlaying() throws RemoteException {
+ return mService.get().isPlaying();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long[] getQueue() throws RemoteException {
+ return mService.get().getQueue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long duration() throws RemoteException {
+ return mService.get().duration();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long position() throws RemoteException {
+ return mService.get().position();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long seek(final long position) throws RemoteException {
+ return mService.get().seek(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getAudioId() throws RemoteException {
+ return mService.get().getAudioId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getArtistId() throws RemoteException {
+ return mService.get().getArtistId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getAlbumId() throws RemoteException {
+ return mService.get().getAlbumId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getArtistName() throws RemoteException {
+ return mService.get().getArtistName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTrackName() throws RemoteException {
+ return mService.get().getTrackName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getAlbumName() throws RemoteException {
+ return mService.get().getAlbumName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPath() throws RemoteException {
+ return mService.get().getPath();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getQueuePosition() throws RemoteException {
+ return mService.get().getQueuePosition();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getShuffleMode() throws RemoteException {
+ return mService.get().getShuffleMode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getRepeatMode() throws RemoteException {
+ return mService.get().getRepeatMode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int removeTracks(final int first, final int last) throws RemoteException {
+ return mService.get().removeTracks(first, last);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int removeTrack(final long id) throws RemoteException {
+ return mService.get().removeTrack(id);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMediaMountedCount() throws RemoteException {
+ return mService.get().getMediaMountedCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getAudioSessionId() throws RemoteException {
+ return mService.get().getAudioSessionId();
+ }
+
+ }
+
+}
diff --git a/src/com/cyngn/eleven/MusicStateListener.java b/src/com/cyngn/eleven/MusicStateListener.java
new file mode 100644
index 0000000..bcf0cf3
--- /dev/null
+++ b/src/com/cyngn/eleven/MusicStateListener.java
@@ -0,0 +1,19 @@
+
+package com.cyngn.eleven;
+
+/**
+ * Listens for playback changes to send the the fragments bound to this activity
+ */
+public interface MusicStateListener {
+
+ /**
+ * Called when {@link MusicPlaybackService#REFRESH} is invoked
+ */
+ public void restartLoader();
+
+ /**
+ * Called when {@link MusicPlaybackService#META_CHANGED} is invoked
+ */
+ public void onMetaChanged();
+
+}
diff --git a/src/com/cyngn/eleven/NotificationHelper.java b/src/com/cyngn/eleven/NotificationHelper.java
new file mode 100644
index 0000000..9460169
--- /dev/null
+++ b/src/com/cyngn/eleven/NotificationHelper.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.cyngn.eleven.utils.ApolloUtils;
+
+/**
+ * Builds the notification for Apollo's service. Jelly Bean and higher uses the
+ * expanded notification by default.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class NotificationHelper {
+
+ /**
+ * Notification ID
+ */
+ private static final int ELEVEN_MUSIC_SERVICE = 1;
+
+ /**
+ * NotificationManager
+ */
+ private final NotificationManager mNotificationManager;
+
+ /**
+ * Context
+ */
+ private final MusicPlaybackService mService;
+
+ /**
+ * Custom notification layout
+ */
+ private RemoteViews mNotificationTemplate;
+
+ /**
+ * The Notification
+ */
+ private Notification mNotification = null;
+
+ /**
+ * API 16+ bigContentView
+ */
+ private RemoteViews mExpandedView;
+
+ /**
+ * Constructor of <code>NotificationHelper</code>
+ *
+ * @param service The {@link Context} to use
+ */
+ public NotificationHelper(final MusicPlaybackService service) {
+ mService = service;
+ mNotificationManager = (NotificationManager)service
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ /**
+ * Call this to build the {@link Notification}.
+ */
+ public void buildNotification(final String albumName, final String artistName,
+ final String trackName, final Long albumId, final Bitmap albumArt,
+ final boolean isPlaying) {
+
+ // Default notfication layout
+ mNotificationTemplate = new RemoteViews(mService.getPackageName(),
+ R.layout.notification_template_base);
+
+ // Set up the content view
+ initCollapsedLayout(trackName, artistName, albumArt);
+
+ // Notification Builder
+ mNotification = new NotificationCompat.Builder(mService)
+ .setSmallIcon(R.drawable.stat_notify_music)
+ .setContentIntent(getPendingIntent())
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setContent(mNotificationTemplate)
+ .build();
+ // Control playback from the notification
+ initPlaybackActions(isPlaying);
+ if (ApolloUtils.hasJellyBean()) {
+ // Expanded notifiction style
+ mExpandedView = new RemoteViews(mService.getPackageName(),
+ R.layout.notification_template_expanded_base);
+ mNotification.bigContentView = mExpandedView;
+ // Control playback from the notification
+ initExpandedPlaybackActions(isPlaying);
+ // Set up the expanded content view
+ initExpandedLayout(trackName, albumName, artistName, albumArt);
+ }
+ mService.startForeground(ELEVEN_MUSIC_SERVICE, mNotification);
+ }
+
+ /**
+ * Remove notification
+ */
+ public void killNotification() {
+ mService.stopForeground(true);
+ mNotification = null;
+ }
+
+ /**
+ * Changes the playback controls in and out of a paused state
+ *
+ * @param isPlaying True if music is playing, false otherwise
+ */
+ public void updatePlayState(final boolean isPlaying) {
+ if (mNotification == null || mNotificationManager == null) {
+ return;
+ }
+ if (mNotificationTemplate != null) {
+ mNotificationTemplate.setImageViewResource(R.id.notification_base_play,
+ isPlaying ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play);
+ }
+
+ if (ApolloUtils.hasJellyBean() && mExpandedView != null) {
+ mExpandedView.setImageViewResource(R.id.notification_expanded_base_play,
+ isPlaying ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play);
+ }
+ mNotificationManager.notify(ELEVEN_MUSIC_SERVICE, mNotification);
+ }
+
+ /**
+ * Open to the now playing screen
+ */
+ private PendingIntent getPendingIntent() {
+ return PendingIntent.getActivity(mService, 0, new Intent("com.cyngn.eleven.AUDIO_PLAYER")
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
+ }
+
+ /**
+ * Lets the buttons in the remote view control playback in the expanded
+ * layout
+ */
+ private void initExpandedPlaybackActions(boolean isPlaying) {
+ // Play and pause
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
+ retreivePlaybackActions(1));
+
+ // Skip tracks
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_next,
+ retreivePlaybackActions(2));
+
+ // Previous tracks
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_previous,
+ retreivePlaybackActions(3));
+
+ // Stop and collapse the notification
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_collapse,
+ retreivePlaybackActions(4));
+
+ // Update the play button image
+ mExpandedView.setImageViewResource(R.id.notification_expanded_base_play,
+ isPlaying ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play);
+ }
+
+ /**
+ * Lets the buttons in the remote view control playback in the normal layout
+ */
+ private void initPlaybackActions(boolean isPlaying) {
+ // Play and pause
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_play,
+ retreivePlaybackActions(1));
+
+ // Skip tracks
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_next,
+ retreivePlaybackActions(2));
+
+ // Previous tracks
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_previous,
+ retreivePlaybackActions(3));
+
+ // Stop and collapse the notification
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_collapse,
+ retreivePlaybackActions(4));
+
+ // Update the play button image
+ mNotificationTemplate.setImageViewResource(R.id.notification_base_play,
+ isPlaying ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play);
+ }
+
+ /**
+ * @param which Which {@link PendingIntent} to return
+ * @return A {@link PendingIntent} ready to control playback
+ */
+ private final PendingIntent retreivePlaybackActions(final int which) {
+ Intent action;
+ PendingIntent pendingIntent;
+ final ComponentName serviceName = new ComponentName(mService, MusicPlaybackService.class);
+ switch (which) {
+ case 1:
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 1, action, 0);
+ return pendingIntent;
+ case 2:
+ // Skip tracks
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 2, action, 0);
+ return pendingIntent;
+ case 3:
+ // Previous tracks
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 3, action, 0);
+ return pendingIntent;
+ case 4:
+ // Stop and collapse the notification
+ action = new Intent(MusicPlaybackService.STOP_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 4, action, 0);
+ return pendingIntent;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Sets the track name, artist name, and album art in the normal layout
+ */
+ private void initCollapsedLayout(final String trackName, final String artistName,
+ final Bitmap albumArt) {
+ // Track name (line one)
+ mNotificationTemplate.setTextViewText(R.id.notification_base_line_one, trackName);
+ // Artist name (line two)
+ mNotificationTemplate.setTextViewText(R.id.notification_base_line_two, artistName);
+ // Album art
+ mNotificationTemplate.setImageViewBitmap(R.id.notification_base_image, albumArt);
+ }
+
+ /**
+ * Sets the track name, album name, artist name, and album art in the
+ * expanded layout
+ */
+ private void initExpandedLayout(final String trackName, final String artistName,
+ final String albumName, final Bitmap albumArt) {
+ // Track name (line one)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_one, trackName);
+ // Album name (line two)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_two, albumName);
+ // Artist name (line three)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_three, artistName);
+ // Album art
+ mExpandedView.setImageViewBitmap(R.id.notification_expanded_base_image, albumArt);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/adapters/AlbumAdapter.java b/src/com/cyngn/eleven/adapters/AlbumAdapter.java
new file mode 100644
index 0000000..6ab4033
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/AlbumAdapter.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.MusicHolder.DataHolder;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the albums on a user's
+ * device for {@link RecentsFragment} and {@link AlbumsFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumAdapter extends ArrayAdapter<Album> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Semi-transparent overlay
+ */
+ private final int mOverlay;
+
+ /**
+ * Determines if the grid or list should be the default style
+ */
+ private boolean mLoadExtraData = false;
+
+ /**
+ * Sets the album art on click listener to start playing them album when
+ * touched.
+ */
+ private boolean mTouchPlay = false;
+
+ /**
+ * Used to cache the album info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>AlbumAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ * @param style Determines which layout to use and therefore which items to
+ * load.
+ */
+ public AlbumAdapter(final Activity context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(context);
+ // Cache the transparent overlay
+ mOverlay = context.getResources().getColor(R.color.list_item_background);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each album name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the artist name (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(dataHolder.mLineTwo, dataHolder.mLineOne, dataHolder.mItemId,
+ holder.mImage.get());
+ // List view only items
+ if (mLoadExtraData) {
+ // Make sure the background layer gets set
+ holder.mOverlay.get().setBackgroundColor(mOverlay);
+ // Set the number of songs (line three)
+ holder.mLineThree.get().setText(dataHolder.mLineThree);
+ // Asynchronously load the artist image on the background view
+ mImageFetcher.loadArtistImage(dataHolder.mLineTwo, holder.mBackground.get());
+ }
+ if (mTouchPlay) {
+ // Play the album when the artwork is touched
+ playAlbum(holder.mImage.get(), position);
+ }
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the album
+ final Album album = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Album Id
+ mData[i].mItemId = album.mAlbumId;
+ // Album names (line one)
+ mData[i].mLineOne = album.mAlbumName;
+ // Album artist names (line two)
+ mData[i].mLineTwo = album.mArtistName;
+ // Number of songs for each album (line three)
+ mData[i].mLineThree = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, album.mSongNumber);
+ }
+ }
+
+ /**
+ * Starts playing an album if the user touches the artwork in the list.
+ *
+ * @param album The {@link ImageView} holding the album
+ * @param position The position of the album to play.
+ */
+ private void playAlbum(final ImageView album, final int position) {
+ album.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final long id = getItem(position).mAlbumId;
+ final long[] list = MusicUtils.getSongListForAlbum(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
+ }
+ });
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param album The key used to find the cached album to remove
+ */
+ public void removeFromCache(final Album album) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(
+ ImageFetcher.generateAlbumCacheKey(album.mAlbumName, album.mArtistName));
+ }
+ }
+
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
+ }
+
+ /**
+ * @param extra True to load line three and the background image, false
+ * otherwise.
+ */
+ public void setLoadExtraData(final boolean extra) {
+ mLoadExtraData = extra;
+ setTouchPlay(true);
+ }
+
+ /**
+ * @param play True to play the album when the artwork is touched, false
+ * otherwise.
+ */
+ public void setTouchPlay(final boolean play) {
+ mTouchPlay = play;
+ }
+}
diff --git a/src/com/cyngn/eleven/adapters/ArtistAdapter.java b/src/com/cyngn/eleven/adapters/ArtistAdapter.java
new file mode 100644
index 0000000..682861a
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/ArtistAdapter.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.MusicHolder.DataHolder;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the artists on a user's
+ * device for {@link ArtistFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAdapter extends ArrayAdapter<Artist> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Semi-transparent overlay
+ */
+ private final int mOverlay;
+
+ /**
+ * Used to cache the artist info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Loads line three and the background image if the user decides to.
+ */
+ private boolean mLoadExtraData = false;
+
+ /**
+ * Constructor of <code>ArtistAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ArtistAdapter(final Activity context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(context);
+ // Cache the transparent overlay
+ mOverlay = context.getResources().getColor(R.color.list_item_background);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each artist name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the number of albums (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(dataHolder.mLineOne, holder.mImage.get());
+ if (mLoadExtraData) {
+ // Make sure the background layer gets set
+ holder.mOverlay.get().setBackgroundColor(mOverlay);
+ // Set the number of songs (line three)
+ holder.mLineThree.get().setText(dataHolder.mLineThree);
+ // Set the background image
+ mImageFetcher.loadArtistImage(dataHolder.mLineOne, holder.mBackground.get());
+ // Play the artist when the artwork is touched
+ playArtist(holder.mImage.get(), position);
+ }
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Artist artist = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Artist Id
+ mData[i].mItemId = artist.mArtistId;
+ // Artist names (line one)
+ mData[i].mLineOne = artist.mArtistName;
+ // Number of albums (line two)
+ mData[i].mLineTwo = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nalbums, artist.mAlbumNumber);
+ // Number of songs (line three)
+ mData[i].mLineThree = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, artist.mSongNumber);
+ }
+ }
+
+ /**
+ * Starts playing an artist if the user touches the artist image in the
+ * list.
+ *
+ * @param artist The {@link ImageView} holding the aritst image
+ * @param position The position of the artist to play.
+ */
+ private void playArtist(final ImageView artist, final int position) {
+ artist.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final long id = getItem(position).mArtistId;
+ final long[] list = MusicUtils.getSongListForArtist(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
+ }
+ });
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param artist The key used to find the cached artist to remove
+ */
+ public void removeFromCache(final Artist artist) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(artist.mArtistName);
+ }
+ }
+
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
+ }
+
+ /**
+ * @param extra True to load line three and the background image, false
+ * otherwise.
+ */
+ public void setLoadExtraData(final boolean extra) {
+ mLoadExtraData = extra;
+ }
+}
diff --git a/src/com/cyngn/eleven/adapters/ArtistAlbumAdapter.java b/src/com/cyngn/eleven/adapters/ArtistAlbumAdapter.java
new file mode 100644
index 0000000..e2fc49e
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/ArtistAlbumAdapter.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.fragments.profile.ArtistAlbumFragment;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This {@link ArrayAdapter} is used to display the albums for a particular
+ * artist for {@link ArtistAlbumFragment} .
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAlbumAdapter extends ArrayAdapter<Album> {
+
+ /**
+ * The header view
+ */
+ private static final int ITEM_VIEW_TYPE_HEADER = 0;
+
+ /**
+ * * The data in the list.
+ */
+ private static final int ITEM_VIEW_TYPE_MUSIC = 1;
+
+ /**
+ * Number of views (ImageView, TextView, header)
+ */
+ private static final int VIEW_TYPE_COUNT = 3;
+
+ /**
+ * LayoutInflater
+ */
+ private final LayoutInflater mInflater;
+
+ /**
+ * Fake header
+ */
+ private final View mHeader;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Used to set the size of the data in the adapter
+ */
+ private List<Album> mCount = Lists.newArrayList();
+
+ /**
+ * Constructor of <code>ArtistAlbumAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ArtistAlbumAdapter(final Activity context, final int layoutId) {
+ super(context, 0);
+ // Used to create the custom layout
+ mInflater = LayoutInflater.from(context);
+ // Cache the header
+ mHeader = mInflater.inflate(R.layout.faux_carousel, null);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+
+ // Return a faux header at position 0
+ if (position == 0) {
+ return mHeader;
+ }
+
+ // Recycle MusicHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Remove the background layer
+ holder.mOverlay.get().setBackgroundColor(0);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the album
+ final Album album = getItem(position - 1);
+ final String albumName = album.mAlbumName;
+
+ // Set each album name (line one)
+ holder.mLineOne.get().setText(albumName);
+ // Set the number of songs (line two)
+ holder.mLineTwo.get().setText(MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, album.mSongNumber));
+ // Set the album year (line three)
+ holder.mLineThree.get().setText(album.mYear);
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(album.mArtistName, albumName, album.mAlbumId,
+ holder.mImage.get());
+ // Play the album when the artwork is touched
+ playAlbum(holder.mImage.get(), position);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ final int size = mCount.size();
+ return size == 0 ? 0 : size + 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ if (position == 0) {
+ return -1;
+ }
+ return position - 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(final int position) {
+ if (position == 0) {
+ return ITEM_VIEW_TYPE_HEADER;
+ }
+ return ITEM_VIEW_TYPE_MUSIC;
+ }
+
+ /**
+ * Starts playing an album if the user touches the artwork in the list.
+ *
+ * @param album The {@link ImageView} holding the album
+ * @param position The position of the album to play.
+ */
+ private void playAlbum(final ImageView album, final int position) {
+ album.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final long id = getItem(position - 1).mAlbumId;
+ final long[] list = MusicUtils.getSongListForAlbum(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
+ }
+ });
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param album The key used to find the cached album to remove
+ */
+ public void removeFromCache(final Album album) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(
+ ImageFetcher.generateAlbumCacheKey(album.mAlbumName, album.mArtistName));
+ }
+ }
+
+ /**
+ * @param data The {@link List} used to return the count for the adapter.
+ */
+ public void setCount(final List<Album> data) {
+ mCount = data;
+ }
+
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
+ }
+}
diff --git a/src/com/cyngn/eleven/adapters/GenreAdapter.java b/src/com/cyngn/eleven/adapters/GenreAdapter.java
new file mode 100644
index 0000000..8d33ab8
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/GenreAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Genre;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.MusicHolder.DataHolder;
+import com.cyngn.eleven.ui.fragments.GenreFragment;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the genres on a user's
+ * device for {@link GenreFragment} .
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreAdapter extends ArrayAdapter<Genre> {
+
+ /**
+ * Number of views (TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 1;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Used to cache the genre info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>GenreAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public GenreAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the second and third lines of text
+ holder.mLineTwo.get().setVisibility(View.GONE);
+ holder.mLineThree.get().setVisibility(View.GONE);
+ // Make line one slightly larger
+ holder.mLineOne.get().setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getContext().getResources().getDimension(R.dimen.text_size_large));
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each genre name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Genre genre = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Genre Id
+ mData[i].mItemId = genre.mGenreId;
+ // Genre names (line one)
+ mData[i].mLineOne = genre.mGenreName;
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/adapters/PagerAdapter.java b/src/com/cyngn/eleven/adapters/PagerAdapter.java
new file mode 100644
index 0000000..d2e06b7
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/PagerAdapter.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.util.SparseArray;
+import android.view.ViewGroup;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.fragments.AlbumFragment;
+import com.cyngn.eleven.ui.fragments.ArtistFragment;
+import com.cyngn.eleven.ui.fragments.GenreFragment;
+import com.cyngn.eleven.ui.fragments.PlaylistFragment;
+import com.cyngn.eleven.ui.fragments.RecentFragment;
+import com.cyngn.eleven.ui.fragments.SongFragment;
+import com.cyngn.eleven.utils.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A {@link FragmentPagerAdapter} class for swiping between playlists, recent,
+ * artists, albums, songs, and genre {@link Fragment}s on phones.<br/>
+ */
+public class PagerAdapter extends FragmentPagerAdapter {
+
+ private final SparseArray<WeakReference<Fragment>> mFragmentArray = new SparseArray<WeakReference<Fragment>>();
+
+ private final List<Holder> mHolderList = Lists.newArrayList();
+
+ private final FragmentActivity mFragmentActivity;
+
+ private int mCurrentPage;
+
+ /**
+ * Constructor of <code>PagerAdatper<code>
+ *
+ * @param fragmentActivity The {@link Activity} of the
+ * {@link Fragment}.
+ */
+ public PagerAdapter(final FragmentActivity fragmentActivity) {
+ super(fragmentActivity.getSupportFragmentManager());
+ mFragmentActivity = fragmentActivity;
+ }
+
+ /**
+ * Method that adds a new fragment class to the viewer (the fragment is
+ * internally instantiate)
+ *
+ * @param className The full qualified name of fragment class.
+ * @param params The instantiate params.
+ */
+ @SuppressWarnings("synthetic-access")
+ public void add(final Class<? extends Fragment> className, final Bundle params) {
+ final Holder mHolder = new Holder();
+ mHolder.mClassName = className.getName();
+ mHolder.mParams = params;
+
+ final int mPosition = mHolderList.size();
+ mHolderList.add(mPosition, mHolder);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Method that returns the {@link Fragment} in the argument
+ * position.
+ *
+ * @param position The position of the fragment to return.
+ * @return Fragment The {@link Fragment} in the argument position.
+ */
+ public Fragment getFragment(final int position) {
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null && mWeakFragment.get() != null) {
+ return mWeakFragment.get();
+ }
+ return getItem(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object instantiateItem(final ViewGroup container, final int position) {
+ final Fragment mFragment = (Fragment)super.instantiateItem(container, position);
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null) {
+ mWeakFragment.clear();
+ }
+ mFragmentArray.put(position, new WeakReference<Fragment>(mFragment));
+ return mFragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Fragment getItem(final int position) {
+ final Holder mCurrentHolder = mHolderList.get(position);
+ final Fragment mFragment = Fragment.instantiate(mFragmentActivity,
+ mCurrentHolder.mClassName, mCurrentHolder.mParams);
+ return mFragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void destroyItem(final ViewGroup container, final int position, final Object object) {
+ super.destroyItem(container, position, object);
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null) {
+ mWeakFragment.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ return mHolderList.size();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CharSequence getPageTitle(final int position) {
+ return mFragmentActivity.getResources().getStringArray(R.array.page_titles)[position]
+ .toUpperCase(Locale.getDefault());
+ }
+
+ /**
+ * Method that returns the current page position.
+ *
+ * @return int The current page.
+ */
+ public int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ /**
+ * Method that sets the current page position.
+ *
+ * @param currentPage The current page.
+ */
+ protected void setCurrentPage(final int currentPage) {
+ mCurrentPage = currentPage;
+ }
+
+ /**
+ * An enumeration of all the main fragments supported.
+ */
+ public enum MusicFragments {
+ /**
+ * The playlist fragment
+ */
+ PLAYLIST(PlaylistFragment.class),
+ /**
+ * The recent fragment
+ */
+ RECENT(RecentFragment.class),
+ /**
+ * The artist fragment
+ */
+ ARTIST(ArtistFragment.class),
+ /**
+ * The album fragment
+ */
+ ALBUM(AlbumFragment.class),
+ /**
+ * The song fragment
+ */
+ SONG(SongFragment.class),
+ /**
+ * The genre fragment
+ */
+ GENRE(GenreFragment.class);
+
+ private Class<? extends Fragment> mFragmentClass;
+
+ /**
+ * Constructor of <code>MusicFragments</code>
+ *
+ * @param fragmentClass The fragment class
+ */
+ private MusicFragments(final Class<? extends Fragment> fragmentClass) {
+ mFragmentClass = fragmentClass;
+ }
+
+ /**
+ * Method that returns the fragment class.
+ *
+ * @return Class<? extends Fragment> The fragment class.
+ */
+ public Class<? extends Fragment> getFragmentClass() {
+ return mFragmentClass;
+ }
+
+ }
+
+ /**
+ * A private class with information about fragment initialization
+ */
+ private final static class Holder {
+ String mClassName;
+
+ Bundle mParams;
+ }
+}
diff --git a/src/com/cyngn/eleven/adapters/PlaylistAdapter.java b/src/com/cyngn/eleven/adapters/PlaylistAdapter.java
new file mode 100644
index 0000000..1a3b498
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/PlaylistAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Playlist;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.MusicHolder.DataHolder;
+import com.cyngn.eleven.ui.fragments.PlaylistFragment;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the playlists on a user's
+ * device for {@link PlaylistFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistAdapter extends ArrayAdapter<Playlist> {
+
+ /**
+ * Number of views (TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 1;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Used to cache the playlist info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>PlaylistAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public PlaylistAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the second and third lines of text
+ holder.mLineTwo.get().setVisibility(View.GONE);
+ holder.mLineThree.get().setVisibility(View.GONE);
+ // Make line one slightly larger
+ holder.mLineOne.get().setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getContext().getResources().getDimension(R.dimen.text_size_large));
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each playlist name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Playlist playlist = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Playlist Id
+ mData[i].mItemId = playlist.mPlaylistId;
+ // Playlist names (line one)
+ mData[i].mLineOne = playlist.mPlaylistName;
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/adapters/ProfileSongAdapter.java b/src/com/cyngn/eleven/adapters/ProfileSongAdapter.java
new file mode 100644
index 0000000..8ed092b
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/ProfileSongAdapter.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.fragments.profile.AlbumSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.ArtistSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.FavoriteFragment;
+import com.cyngn.eleven.ui.fragments.profile.GenreSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.LastAddedFragment;
+import com.cyngn.eleven.ui.fragments.profile.PlaylistSongFragment;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This {@link ArrayAdapter} is used to display the songs for a particular
+ * artist, album, playlist, or genre for {@link ArtistSongFragment},
+ * {@link AlbumSongFragment},{@link PlaylistSongFragment},
+ * {@link GenreSongFragment},{@link FavoriteFragment},{@link LastAddedFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ProfileSongAdapter extends ArrayAdapter<Song> {
+
+ /**
+ * Default display setting: title/album
+ */
+ public static final int DISPLAY_DEFAULT_SETTING = 0;
+
+ /**
+ * Playlist display setting: title/artist-album
+ */
+ public static final int DISPLAY_PLAYLIST_SETTING = 1;
+
+ /**
+ * Album display setting: title/duration
+ */
+ public static final int DISPLAY_ALBUM_SETTING = 2;
+
+ /**
+ * The header view
+ */
+ private static final int ITEM_VIEW_TYPE_HEADER = 0;
+
+ /**
+ * * The data in the list.
+ */
+ private static final int ITEM_VIEW_TYPE_MUSIC = 1;
+
+ /**
+ * Number of views (ImageView, TextView, header)
+ */
+ private static final int VIEW_TYPE_COUNT = 3;
+
+ /**
+ * LayoutInflater
+ */
+ private final LayoutInflater mInflater;
+
+ /**
+ * Fake header
+ */
+ private final View mHeader;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Display setting for the second line in a song fragment
+ */
+ private final int mDisplaySetting;
+
+ /**
+ * Separator used for separating album/artist strings
+ */
+ private final String SEPARATOR_STRING = " - ";
+
+ /**
+ * Used to set the size of the data in the adapter
+ */
+ private List<Song> mCount = Lists.newArrayList();
+
+ /**
+ * Constructor of <code>ProfileSongAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ * @param setting defines the content of the second line
+ */
+ public ProfileSongAdapter(final Context context, final int layoutId, final int setting) {
+ super(context, 0);
+ // Used to create the custom layout
+ mInflater = LayoutInflater.from(context);
+ // Cache the header
+ mHeader = mInflater.inflate(R.layout.faux_carousel, null);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Know what to put in line two
+ mDisplaySetting = setting;
+ }
+
+ /**
+ * Constructor of <code>ProfileSongAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ProfileSongAdapter(final Context context, final int layoutId) {
+ this(context, layoutId, DISPLAY_DEFAULT_SETTING);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+
+ // Return a faux header at position 0
+ if (position == 0) {
+ return mHeader;
+ }
+
+ // Recycle MusicHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the third line of text
+ holder.mLineThree.get().setVisibility(View.GONE);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the album
+ final Song song = getItem(position - 1);
+
+ // Set each track name (line one)
+ holder.mLineOne.get().setText(song.mSongName);
+ // Set the line two
+ switch (mDisplaySetting) {
+ // show duration if on album fragment
+ case DISPLAY_ALBUM_SETTING:
+ holder.mLineOneRight.get().setVisibility(View.GONE);
+
+ holder.mLineTwo.get().setText(
+ MusicUtils.makeTimeString(getContext(), song.mDuration));
+ break;
+ case DISPLAY_PLAYLIST_SETTING:
+ if (song.mDuration == -1) {
+ holder.mLineOneRight.get().setVisibility(View.GONE);
+ } else {
+ holder.mLineOneRight.get().setVisibility(View.VISIBLE);
+ holder.mLineOneRight.get().setText(
+ MusicUtils.makeTimeString(getContext(), song.mDuration));
+ }
+
+ final StringBuilder sb = new StringBuilder(song.mArtistName);
+ sb.append(SEPARATOR_STRING);
+ sb.append(song.mAlbumName);
+ holder.mLineTwo.get().setText(sb.toString());
+ break;
+ case DISPLAY_DEFAULT_SETTING:
+ default:
+ holder.mLineOneRight.get().setVisibility(View.VISIBLE);
+
+ holder.mLineOneRight.get().setText(
+ MusicUtils.makeTimeString(getContext(), song.mDuration));
+ holder.mLineTwo.get().setText(song.mAlbumName);
+ break;
+ }
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ final int size = mCount.size();
+ return size == 0 ? 0 : size + 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ if (position == 0) {
+ return -1;
+ }
+ return position - 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(final int position) {
+ if (position == 0) {
+ return ITEM_VIEW_TYPE_HEADER;
+ }
+ return ITEM_VIEW_TYPE_MUSIC;
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ }
+
+ /**
+ * @param data The {@link List} used to return the count for the adapter.
+ */
+ public void setCount(final List<Song> data) {
+ mCount = data;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/adapters/SongAdapter.java b/src/com/cyngn/eleven/adapters/SongAdapter.java
new file mode 100644
index 0000000..bc2684c
--- /dev/null
+++ b/src/com/cyngn/eleven/adapters/SongAdapter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.ui.MusicHolder.DataHolder;
+import com.cyngn.eleven.ui.fragments.QueueFragment;
+import com.cyngn.eleven.ui.fragments.SongFragment;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the songs on a user's
+ * device for {@link SongFragment}. It is also used to show the queue in
+ * {@link QueueFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongAdapter extends ArrayAdapter<Song> {
+
+ /**
+ * Number of views (TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 1;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Used to cache the song info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>SongAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public SongAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the third line of text
+ holder.mLineThree.get().setVisibility(View.GONE);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each song name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the song duration (line one, right)
+ holder.mLineOneRight.get().setText(dataHolder.mLineOneRight);
+ // Set the album name (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the song
+ final Song song = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Song Id
+ mData[i].mItemId = song.mSongId;
+ // Song names (line one)
+ mData[i].mLineOne = song.mSongName;
+ // Song duration (line one, right)
+ mData[i].mLineOneRight = MusicUtils.makeTimeString(getContext(), song.mDuration);
+ // Album names (line two)
+ mData[i].mLineTwo = song.mAlbumName;
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/AppWidgetBase.java b/src/com/cyngn/eleven/appwidgets/AppWidgetBase.java
new file mode 100644
index 0000000..89be2f2
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/AppWidgetBase.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.cyngn.eleven.MusicPlaybackService;
+
+public abstract class AppWidgetBase extends AppWidgetProvider {
+
+ protected PendingIntent buildPendingIntent(Context context, final String action,
+ final ComponentName serviceName) {
+ Intent intent = new Intent(action);
+ intent.setComponent(serviceName);
+ intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, false);
+ return PendingIntent.getService(context, 0, intent, 0);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/AppWidgetLarge.java b/src/com/cyngn/eleven/appwidgets/AppWidgetLarge.java
new file mode 100644
index 0000000..54be883
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/AppWidgetLarge.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.widget.RemoteViews;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.AudioPlayerActivity;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+
+/**
+ * 4x2 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetLarge extends AppWidgetBase {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_large_update";
+
+ private static AppWidgetLarge mInstance;
+
+ public static synchronized AppWidgetLarge getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetLarge();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME, AppWidgetLarge.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_large);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_large);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final CharSequence albumName = service.getAlbumName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_two, artistName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_three, albumName);
+ appWidgetView.setImageViewBitmap(R.id.app_widget_large_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_image, pendingIntent);
+ }
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.PREVIOUS_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_previous, pendingIntent);
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.TOGGLEPAUSE_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_play, pendingIntent);
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.NEXT_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/AppWidgetLargeAlternate.java b/src/com/cyngn/eleven/appwidgets/AppWidgetLargeAlternate.java
new file mode 100644
index 0000000..a6354a5
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/AppWidgetLargeAlternate.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.widget.RemoteViews;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.AudioPlayerActivity;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+
+/**
+ * 4x2 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetLargeAlternate extends AppWidgetBase {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_large_alternate_update";
+
+ private static AppWidgetLargeAlternate mInstance;
+
+ public static synchronized AppWidgetLargeAlternate getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetLargeAlternate();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME,
+ AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_large_alternate);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)
+ || MusicPlaybackService.REPEATMODE_CHANGED.equals(what)
+ || MusicPlaybackService.SHUFFLEMODE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_large_alternate);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final CharSequence albumName = service.getAlbumName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_two, artistName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_three, albumName);
+ appWidgetView.setImageViewBitmap(R.id.app_widget_large_alternate_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_alternate_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_alternate_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Set the correct drawable for the repeat state
+ switch (service.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_ALL:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat_all);
+ break;
+ case MusicPlaybackService.REPEAT_CURRENT:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat_one);
+ break;
+ default:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat);
+ break;
+ }
+
+ // Set the correct drawable for the shuffle state
+ switch (service.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NONE:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle);
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle_all);
+ break;
+ default:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle_all);
+ break;
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_info_container,
+ pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_info_container,
+ pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_image, pendingIntent);
+ }
+ // Shuffle modes
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.SHUFFLE_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_shuffle, pendingIntent);
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.PREVIOUS_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_previous, pendingIntent);
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.TOGGLEPAUSE_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_play, pendingIntent);
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.NEXT_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_next, pendingIntent);
+
+ // Repeat modes
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.REPEAT_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_repeat, pendingIntent);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/AppWidgetSmall.java b/src/com/cyngn/eleven/appwidgets/AppWidgetSmall.java
new file mode 100644
index 0000000..368f223
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/AppWidgetSmall.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.AudioPlayerActivity;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+
+/**
+ * 4x1 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetSmall extends AppWidgetBase {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_small_update";
+
+ private static AppWidgetSmall mInstance;
+
+ public static synchronized AppWidgetSmall getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetSmall();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME, AppWidgetSmall.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_small);
+ appWidgetViews.setViewVisibility(R.id.app_widget_small_info_container, View.INVISIBLE);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_small);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ if (TextUtils.isEmpty(trackName) && TextUtils.isEmpty(artistName)) {
+ appWidgetView.setViewVisibility(R.id.app_widget_small_info_container, View.INVISIBLE);
+ } else {
+ appWidgetView.setViewVisibility(R.id.app_widget_small_info_container, View.VISIBLE);
+ appWidgetView.setTextViewText(R.id.app_widget_small_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_small_line_two, artistName);
+ }
+ appWidgetView.setImageViewBitmap(R.id.app_widget_small_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_small_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_small_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_small_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_small_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_small_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_small_image, pendingIntent);
+ }
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.PREVIOUS_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_small_previous, pendingIntent);
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.TOGGLEPAUSE_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_small_play, pendingIntent);
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.NEXT_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_small_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/RecentWidgetProvider.java b/src/com/cyngn/eleven/appwidgets/RecentWidgetProvider.java
new file mode 100644
index 0000000..9a80489
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/RecentWidgetProvider.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.MediaStore;
+import android.widget.RemoteViews;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.AudioPlayerActivity;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.ui.activities.ShortcutActivity;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * App-Widget used to display a list of recently listened albums.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@TargetApi(11)
+public class RecentWidgetProvider extends AppWidgetBase {
+
+ public static final String SET_ACTION = "set_action";
+
+ public static final String OPEN_PROFILE = "open_profile";
+
+ public static final String PLAY_ALBUM = "play_album";
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_recents_update";
+
+ public static final String CLICK_ACTION = "com.cyngn.eleven.recents.appwidget.action.CLICK";
+
+ public static final String REFRESH_ACTION = "com.cyngn.eleven.recents.appwidget.action.REFRESH";
+
+ private static Handler sWorkerQueue;
+
+ private static RecentWidgetProvider mInstance;
+
+ private RemoteViews mViews;
+
+ /**
+ * Constructor of <code>RecentWidgetProvider</code>
+ */
+ public RecentWidgetProvider() {
+ // Start the worker thread
+ final HandlerThread workerThread = new HandlerThread("RecentWidgetProviderWorker",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ workerThread.start();
+ sWorkerQueue = new Handler(workerThread.getLooper());
+ }
+
+ /**
+ * @return A singelton of {@link RecentWidgetProvider}
+ */
+ public static synchronized RecentWidgetProvider getInstance() {
+ if (mInstance == null) {
+ mInstance = new RecentWidgetProvider();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ for (final int appWidgetId : appWidgetIds) {
+ // Create the remote views
+ mViews = new RemoteViews(context.getPackageName(), R.layout.app_widget_recents);
+
+ // Link actions buttons to intents
+ linkButtons(context, mViews, false);
+
+ final Intent intent = new Intent(context, RecentWidgetService.class);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ compatSetRemoteAdapter(mViews, appWidgetId, intent);
+
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME,
+ RecentWidgetProvider.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+
+ final Intent onClickIntent = new Intent(context, RecentWidgetProvider.class);
+ onClickIntent.setAction(RecentWidgetProvider.CLICK_ACTION);
+ onClickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ onClickIntent.setData(Uri.parse(onClickIntent.toUri(Intent.URI_INTENT_SCHEME)));
+ final PendingIntent onClickPendingIntent = PendingIntent.getBroadcast(context, 0,
+ onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ mViews.setPendingIntentTemplate(R.id.app_widget_recents_list, onClickPendingIntent);
+
+ // Update the widget
+ appWidgetManager.updateAppWidget(appWidgetId, mViews);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+
+ if (CLICK_ACTION.equals(action)) {
+ final long albumId = intent.getLongExtra(Config.ID, -1);
+
+ if (intent.getStringExtra(SET_ACTION).equals(PLAY_ALBUM)) {
+ // Play the selected album
+ if (albumId != -1) {
+ final Intent shortcutIntent = new Intent(context, ShortcutActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_VIEW);
+ shortcutIntent.putExtra(Config.ID, albumId);
+ shortcutIntent.putExtra(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ shortcutIntent.putExtra(ShortcutActivity.OPEN_AUDIO_PLAYER, false);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(shortcutIntent);
+ }
+ } else if (intent.getStringExtra(SET_ACTION).equals(OPEN_PROFILE)) {
+ final String albumName = intent.getStringExtra(Config.NAME);
+ // Transfer the album name and MIME type
+ final Bundle bundle = new Bundle();
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ bundle.putString(Config.NAME, albumName);
+ bundle.putString(Config.ARTIST_NAME, intent.getStringExtra(Config.ARTIST_NAME));
+ bundle.putString(Config.ALBUM_YEAR,
+ MusicUtils.getReleaseDateForAlbum(context, albumId));
+ bundle.putLong(Config.ID, albumId);
+
+ // Open the album profile
+ final Intent profileIntent = new Intent(context, ProfileActivity.class);
+ profileIntent.putExtras(bundle);
+ profileIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ profileIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (albumId != -1) {
+ context.startActivity(profileIntent);
+ }
+ }
+
+ }
+ super.onReceive(context, intent);
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void compatSetRemoteAdapter(final RemoteViews rv, final int appWidgetId,
+ final Intent intent) {
+ rv.setRemoteAdapter(R.id.app_widget_recents_list, intent);
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, this
+ .getClass()));
+ return appWidgetIds.length > 0;
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ } else if (MusicPlaybackService.META_CHANGED.equals(what)) {
+ synchronized (service) {
+ sWorkerQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ final AppWidgetManager appWidgetManager = AppWidgetManager
+ .getInstance(service);
+ final ComponentName componentName = new ComponentName(service,
+ RecentWidgetProvider.class);
+ appWidgetManager.notifyAppWidgetViewDataChanged(
+ appWidgetManager.getAppWidgetIds(componentName),
+ R.id.app_widget_recents_list);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ mViews = new RemoteViews(service.getPackageName(), R.layout.app_widget_recents);
+
+ /* Set correct drawable for pause state */
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ mViews.setImageViewResource(R.id.app_widget_recents_play, R.drawable.btn_playback_pause);
+ } else {
+ mViews.setImageViewResource(R.id.app_widget_recents_play, R.drawable.btn_playback_play);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, mViews, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, mViews);
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_action_bar, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_action_bar, pendingIntent);
+ }
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.PREVIOUS_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_previous, pendingIntent);
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.TOGGLEPAUSE_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_play, pendingIntent);
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, MusicPlaybackService.NEXT_ACTION, serviceName);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/appwidgets/RecentWidgetService.java b/src/com/cyngn/eleven/appwidgets/RecentWidgetService.java
new file mode 100644
index 0000000..d687358
--- /dev/null
+++ b/src/com/cyngn/eleven/appwidgets/RecentWidgetService.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.appwidgets;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.provider.RecentStore;
+import com.cyngn.eleven.provider.RecentStore.RecentStoreColumns;
+
+/**
+ * This class is used to build the recently listened list for the
+ * {@link RecentWidgetProvicer}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@TargetApi(11)
+public class RecentWidgetService extends RemoteViewsService {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViewsFactory onGetViewFactory(final Intent intent) {
+ return new WidgetRemoteViewsFactory(getApplicationContext());
+ }
+
+ /**
+ * This is the factory that will provide data to the collection widget.
+ */
+ private static final class WidgetRemoteViewsFactory implements
+ RemoteViewsService.RemoteViewsFactory {
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The context to use
+ */
+ private final Context mContext;
+
+ /**
+ * Image cache
+ */
+ private final ImageFetcher mFetcher;
+
+ /**
+ * Recents db
+ */
+ private final RecentStore mRecentsStore;
+
+ /**
+ * Cursor to use
+ */
+ private Cursor mCursor;
+
+ /**
+ * Remove views
+ */
+ private RemoteViews mViews;
+
+ /**
+ * Constructor of <code>WidgetRemoteViewsFactory</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public WidgetRemoteViewsFactory(final Context context) {
+ // Get the context
+ mContext = context;
+ // Initialze the image cache
+ mFetcher = ImageFetcher.getInstance(context);
+ mFetcher.setImageCache(ImageCache.getInstance(context));
+ // Initialze the recents store
+ mRecentsStore = RecentStore.getInstance(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ // Check for errors
+ if (mCursor == null || mCursor.isClosed() || mCursor.getCount() <= 0) {
+ return 0;
+ }
+ return mCursor.getCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ return position;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViews getViewAt(final int position) {
+ mCursor.moveToPosition(position);
+
+ // Create the remote views
+ mViews = new RemoteViews(mContext.getPackageName(), R.layout.app_widget_recents_items);
+
+ // Copy the album id
+ final long id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ID));
+
+ // Copy the album name
+ final String albumName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ARTISTNAME));
+
+ // Set the album names
+ mViews.setTextViewText(R.id.app_widget_recents_line_one, albumName);
+ // Set the artist names
+ mViews.setTextViewText(R.id.app_widget_recents_line_two, artist);
+ // Set the album art
+ Bitmap bitmap = mFetcher.getCachedArtwork(albumName, artist, id);
+ if (bitmap != null) {
+ mViews.setImageViewBitmap(R.id.app_widget_recents_base_image, bitmap);
+ } else {
+ mViews.setImageViewResource(R.id.app_widget_recents_base_image,
+ R.drawable.default_artwork);
+ }
+
+ // Open the profile of the touched album
+ final Intent profileIntent = new Intent();
+ final Bundle profileExtras = new Bundle();
+ profileExtras.putLong(Config.ID, id);
+ profileExtras.putString(Config.NAME, albumName);
+ profileExtras.putString(Config.ARTIST_NAME, artist);
+ profileExtras.putString(RecentWidgetProvider.SET_ACTION,
+ RecentWidgetProvider.OPEN_PROFILE);
+ profileIntent.putExtras(profileExtras);
+ mViews.setOnClickFillInIntent(R.id.app_widget_recents_items, profileIntent);
+
+ // Play the album when the artwork is touched
+ final Intent playAlbum = new Intent();
+ final Bundle playAlbumExtras = new Bundle();
+ playAlbumExtras.putLong(Config.ID, id);
+ playAlbumExtras.putString(RecentWidgetProvider.SET_ACTION,
+ RecentWidgetProvider.PLAY_ALBUM);
+ playAlbum.putExtras(playAlbumExtras);
+ mViews.setOnClickFillInIntent(R.id.app_widget_recents_base_image, playAlbum);
+ return mViews;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDataSetChanged() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+ mCursor = mRecentsStore.getReadableDatabase().query(
+ RecentStoreColumns.NAME,
+ new String[] {
+ RecentStoreColumns.ID + " as id", RecentStoreColumns.ID,
+ RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.ALBUMSONGCOUNT, RecentStoreColumns.ALBUMYEAR,
+ RecentStoreColumns.TIMEPLAYED
+ }, null, null, null, null, RecentStoreColumns.TIMEPLAYED + " DESC");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroy() {
+ closeCursor();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViews getLoadingView() {
+ // Nothing to do
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ // Nothing to do
+ }
+
+ private void closeCursor() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/cache/DiskLruCache.java b/src/com/cyngn/eleven/cache/DiskLruCache.java
new file mode 100644
index 0000000..1b73447
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/DiskLruCache.java
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.cache;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ ****************************************************************************** Taken from the JB source code, can be found in:
+ * libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link:
+ * https:
+ * //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/
+ * main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of
+ * space on a filesystem. Each cache entry has a string key and a fixed number
+ * of values. Values are byte sequences, accessible as streams or files. Each
+ * value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in
+ * length.
+ * <p>
+ * The cache stores its data in a directory on the filesystem. This directory
+ * must be exclusive to the cache; the cache may delete or overwrite files from
+ * its directory. It is an error for multiple processes to use the same cache
+ * directory at the same time.
+ * <p>
+ * This cache limits the number of bytes that it will store on the filesystem.
+ * When the number of stored bytes exceeds the limit, the cache will remove
+ * entries in the background until the limit is satisfied. The limit is not
+ * strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache journal
+ * so space-sensitive applications should set a conservative limit.
+ * <p>
+ * Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ * <li>When an entry is being <strong>created</strong> it is necessary to supply
+ * a full set of values; the empty value should be used as a placeholder if
+ * necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary to
+ * supply data for every value; values default to their previous value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ * <p>
+ * Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ * <p>
+ * This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If an
+ * error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+
+ static final String JOURNAL_FILE_TMP = "journal.tmp";
+
+ static final String MAGIC = "libcore.io.DiskLruCache";
+
+ static final String VERSION_1 = "1";
+
+ static final long ANY_SEQUENCE_NUMBER = -1;
+
+ private static final String CLEAN = "CLEAN";
+
+ private static final String DIRTY = "DIRTY";
+
+ private static final String REMOVE = "REMOVE";
+
+ private static final String READ = "READ";
+
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private static final int IO_BUFFER_SIZE = 8 * 1024;
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY
+ * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52
+ * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY
+ * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a
+ * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form
+ * its header. They are the constant string "libcore.io.DiskLruCache", the
+ * disk cache's version, the application's version, the value count, and a
+ * blank line. Each of the subsequent lines in the file is a record of the
+ * state of a cache entry. Each line contains space-separated values: a
+ * state, a key, and optional state-specific values. o DIRTY lines track
+ * that an entry is actively being created or updated. Every successful
+ * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines
+ * without a matching CLEAN or REMOVE indicate that temporary files may need
+ * to be deleted. o CLEAN lines track a cache entry that has been
+ * successfully published and may be read. A publish line is followed by the
+ * lengths of each of its values. o READ lines track accesses for LRU. o
+ * REMOVE lines track entries that have been deleted. The journal file is
+ * appended to as cache operations occur. The journal may occasionally be
+ * compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted
+ * if it exists when the cache is opened.
+ */
+
+ private final File directory;
+
+ private final File journalFile;
+
+ private final File journalFileTmp;
+
+ private final int appVersion;
+
+ private final long maxSize;
+
+ private final int valueCount;
+
+ private long size = 0;
+
+ private Writer journalWriter;
+
+ private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0,
+ 0.75f, true);
+
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given a
+ * sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /* From java.util.Arrays */
+ @SuppressWarnings("unchecked")
+ private static <T> T[] copyOfRange(final T[] original, final int start, final int end) {
+ final int originalLength = original.length; // For exception priority
+ // compatibility.
+ if (start > end) {
+ throw new IllegalArgumentException();
+ }
+ if (start < 0 || start > originalLength) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ final int resultLength = end - start;
+ final int copyLength = Math.min(resultLength, originalLength - start);
+ final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(),
+ resultLength);
+ System.arraycopy(original, start, result, 0, copyLength);
+ return result;
+ }
+
+ /**
+ * Returns the remainder of 'reader' as a string, closing it when done.
+ */
+ public static String readFully(final Reader reader) throws IOException {
+ try {
+ final StringWriter writer = new StringWriter();
+ final char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * Returns the ASCII characters up to but not including the next "\r\n", or
+ * "\n".
+ *
+ * @throws java.io.EOFException if the stream is exhausted before the next
+ * newline character.
+ */
+ public static String readAsciiLine(final InputStream in) throws IOException {
+ // TODO: support UTF-8 here instead
+
+ final StringBuilder result = new StringBuilder(80);
+ while (true) {
+ final int c = in.read();
+ if (c == -1) {
+ throw new EOFException();
+ } else if (c == '\n') {
+ break;
+ }
+
+ result.append((char)c);
+ }
+ final int length = result.length();
+ if (length > 0 && result.charAt(length - 1) == '\r') {
+ result.setLength(length - 1);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Closes 'closeable', ignoring any checked exceptions. Does nothing if
+ * 'closeable' is null.
+ */
+ public static void closeQuietly(final Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (final RuntimeException rethrown) {
+ throw rethrown;
+ } catch (final Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Recursively delete everything in {@code dir}.
+ */
+ // TODO: this should specify paths as Strings rather than as Files
+ public static void deleteContents(final File dir) throws IOException {
+ final File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IllegalArgumentException("not a directory: " + dir);
+ }
+ for (final File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (!file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ /** This cache uses a single background thread to evict entries. */
+ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L,
+ TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ synchronized (DiskLruCache.this) {
+ if (journalWriter == null) {
+ return null; // closed
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private DiskLruCache(final File directory, final int appVersion, final int valueCount,
+ final long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ journalFile = new File(directory, JOURNAL_FILE);
+ journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param appVersion
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static DiskLruCache open(final File directory, final int appVersion,
+ final int valueCount, final long maxSize) throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // prefer to pick up where we left off
+ DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
+ IO_BUFFER_SIZE);
+ return cache;
+ } catch (final IOException journalIsCorrupt) {
+ // System.logW("DiskLruCache " + directory + " is corrupt: "
+ // + journalIsCorrupt.getMessage() + ", removing");
+ cache.delete();
+ }
+ }
+
+ // create a new empty cache
+ directory.mkdirs();
+ cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
+ IO_BUFFER_SIZE);
+ try {
+ final String magic = readAsciiLine(in);
+ final String version = readAsciiLine(in);
+ final String appVersionString = readAsciiLine(in);
+ final String valueCountString = readAsciiLine(in);
+ final String blank = readAsciiLine(in);
+ if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version
+ + ", " + valueCountString + ", " + blank + "]");
+ }
+
+ while (true) {
+ try {
+ readJournalLine(readAsciiLine(in));
+ } catch (final EOFException endOfJournal) {
+ break;
+ }
+ }
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ private void readJournalLine(final String line) throws IOException {
+ final String[] parts = line.split(" ");
+ if (parts.length < 2) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ final String key = parts[1];
+ if (parts[0].equals(REMOVE) && parts.length == 2) {
+ lruEntries.remove(key);
+ return;
+ }
+
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ }
+
+ if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+ entry.readable = true;
+ entry.currentEditor = null;
+ entry.setLengths(copyOfRange(parts, 2, parts.length));
+ } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+ entry.currentEditor = new Editor(entry);
+ } else if (parts[0].equals(READ) && parts.length == 2) {
+ // this work was already done by calling lruEntries.get()
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
+ final Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ journalWriter.close();
+ }
+
+ final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (final Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
+ }
+
+ writer.close();
+ journalFileTmp.renameTo(journalFile);
+ journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
+ }
+
+ private static void deleteIfExists(final File file) throws IOException {
+ // try {
+ // Libcore.os.remove(file.getPath());
+ // } catch (ErrnoException errnoException) {
+ // if (errnoException.errno != OsConstants.ENOENT) {
+ // throw errnoException.rethrowAsIOException();
+ // }
+ // }
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ public synchronized Snapshot get(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ /*
+ * Open all streams eagerly to guarantee that we see a single published
+ * snapshot. If we opened streams lazily then the streams could come
+ * from different edits.
+ */
+ final InputStream[] ins = new InputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (final FileNotFoundException e) {
+ // a file must have been deleted manually!
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + ' ' + key + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(key, entry.sequenceNumber, ins);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(final String key) throws IOException {
+ return edit(key, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor edit(final String key, final long expectedSequenceNumber)
+ throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
+ && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // snapshot is stale
+ }
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // another edit is in progress
+ }
+
+ final Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // flush the journal before creating files to prevent file leaks
+ journalWriter.write(DIRTY + ' ' + key + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /**
+ * Returns the directory where this cache stores its data.
+ */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public long maxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(final Editor editor, final boolean success)
+ throws IOException {
+ final Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // if this edit is creating the entry for the first time, every index
+ // must have a value
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ throw new IllegalStateException("edit didn't create file " + i);
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ final File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ final long oldLength = entry.lengths[i];
+ final long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.key);
+ journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+ }
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
+ return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ /**
+ * Drops the entry for {@code key} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ public synchronized boolean remove(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File file = entry.getCleanFile(i);
+ if (!file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(REMOVE + ' ' + key + '\n');
+ lruEntries.remove(key);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if this cache has been closed.
+ */
+ public boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /**
+ * Force buffered operations to the filesystem.
+ */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /**
+ * Closes this cache. Stored values will remain on the filesystem.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // already closed
+ }
+ for (final Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ // Map.Entry<String, Entry> toEvict = lruEntries.eldest();
+ final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+ remove(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ close();
+ deleteContents(directory);
+ }
+
+ private void validateKey(final String key) {
+ if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
+ throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key
+ + "\"");
+ }
+ }
+
+ private static String inputStreamToString(final InputStream in) throws IOException {
+ return readFully(new InputStreamReader(in, UTF_8));
+ }
+
+ /**
+ * A snapshot of the values for an entry.
+ */
+ public final class Snapshot implements Closeable {
+ private final String key;
+
+ private final long sequenceNumber;
+
+ private final InputStream[] ins;
+
+ private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) {
+ this.key = key;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return DiskLruCache.this.edit(key, sequenceNumber);
+ }
+
+ /**
+ * Returns the unbuffered stream with the value for {@code index}.
+ */
+ public InputStream getInputStream(final int index) {
+ return ins[index];
+ }
+
+ /**
+ * Returns the string value for {@code index}.
+ */
+ public String getString(final int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ @Override
+ public void close() {
+ for (final InputStream in : ins) {
+ closeQuietly(in);
+ }
+ }
+ }
+
+ /**
+ * Edits the values for an entry.
+ */
+ public final class Editor {
+ private final Entry entry;
+
+ private boolean hasErrors;
+
+ private Editor(final Entry entry) {
+ this.entry = entry;
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ return new FileInputStream(entry.getCleanFile(index));
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value has
+ * been committed.
+ */
+ public String getString(final int index) throws IOException {
+ final InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors when
+ * writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+ }
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}.
+ */
+ public void set(final int index, final String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+ writer.write(value);
+ } finally {
+ closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the edit
+ * lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // the previous entry is stale
+ } else {
+ completeEdit(this, true);
+ }
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(final OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(final int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void write(final byte[] buffer, final int offset, final int length) {
+ try {
+ out.write(buffer, offset, length);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ out.close();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void flush() {
+ try {
+ out.flush();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String key;
+
+ /** Lengths of this entry's files. */
+ private final long[] lengths;
+
+ /** True if this entry has ever been published */
+ private boolean readable;
+
+ /** The ongoing edit or null if this entry is not being edited. */
+ private Editor currentEditor;
+
+ /**
+ * The sequence number of the most recently committed edit to this
+ * entry.
+ */
+ private long sequenceNumber;
+
+ private Entry(final String key) {
+ this.key = key;
+ lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ final StringBuilder result = new StringBuilder();
+ for (final long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Set lengths using decimal numbers like "10123".
+ */
+ private void setLengths(final String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
+ }
+ } catch (final NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(final String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+ }
+
+ public File getCleanFile(final int i) {
+ return new File(directory, key + "." + i);
+ }
+
+ public File getDirtyFile(final int i) {
+ return new File(directory, key + "." + i + ".tmp");
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/cache/ImageCache.java b/src/com/cyngn/eleven/cache/ImageCache.java
new file mode 100644
index 0000000..f8b66ac
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/ImageCache.java
@@ -0,0 +1,780 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.cache;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.cyngn.eleven.utils.ApolloUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * This class holds the memory and disk bitmap caches.
+ */
+public final class ImageCache {
+
+ private static final String TAG = ImageCache.class.getSimpleName();
+
+ /**
+ * The {@link Uri} used to retrieve album art
+ */
+ private static final Uri mArtworkUri;
+
+ /**
+ * Default memory cache size as a percent of device memory class
+ */
+ private static final float MEM_CACHE_DIVIDER = 0.25f;
+
+ /**
+ * Default disk cache size 10MB
+ */
+ private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
+
+ /**
+ * Compression settings when writing images to disk cache
+ */
+ private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
+
+ /**
+ * Disk cache index to read from
+ */
+ private static final int DISK_CACHE_INDEX = 0;
+
+ /**
+ * Image compression quality
+ */
+ private static final int COMPRESS_QUALITY = 98;
+
+ /**
+ * LRU cache
+ */
+ private MemoryCache mLruCache;
+
+ /**
+ * Disk LRU cache
+ */
+ private DiskLruCache mDiskCache;
+
+ private static ImageCache sInstance;
+
+ /**
+ * Used to temporarily pause the disk cache while scrolling
+ */
+ public boolean mPauseDiskAccess = false;
+ private Object mPauseLock = new Object();
+
+ static {
+ mArtworkUri = Uri.parse("content://media/external/audio/albumart");
+ }
+
+ /**
+ * Constructor of <code>ImageCache</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public ImageCache(final Context context) {
+ init(context);
+ }
+
+ /**
+ * Used to create a singleton of {@link ImageCache}
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public final static ImageCache getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageCache(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Initialize the cache, providing all parameters.
+ *
+ * @param context The {@link Context} to use
+ * @param cacheParams The cache parameters to initialize the cache
+ */
+ private void init(final Context context) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Initialize the disk cahe in a background thread
+ initDiskCache(context);
+ return null;
+ }
+ }, (Void[])null);
+ // Set up the memory cache
+ initLruCache(context);
+ }
+
+ /**
+ * Initializes the disk cache. Note that this includes disk access so this
+ * should not be executed on the main/UI thread. By default an ImageCache
+ * does not initialize the disk cache when it is created, instead you should
+ * call initDiskCache() to initialize it on a background thread.
+ *
+ * @param context The {@link Context} to use
+ */
+ private synchronized void initDiskCache(final Context context) {
+ // Set up disk cache
+ if (mDiskCache == null || mDiskCache.isClosed()) {
+ File diskCacheDir = getDiskCacheDir(context, TAG);
+ if (diskCacheDir != null) {
+ if (!diskCacheDir.exists()) {
+ diskCacheDir.mkdirs();
+ }
+ if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
+ try {
+ mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
+ } catch (final IOException e) {
+ diskCacheDir = null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up the Lru cache
+ *
+ * @param context The {@link Context} to use
+ */
+ @SuppressLint("NewApi")
+ public void initLruCache(final Context context) {
+ final ActivityManager activityManager = (ActivityManager)context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
+ * 1024 * 1024);
+ mLruCache = new MemoryCache(lruCacheSize);
+
+ // Release some memory as needed
+ context.registerComponentCallbacks(new ComponentCallbacks2() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTrimMemory(final int level) {
+ if (level >= TRIM_MEMORY_MODERATE) {
+ evictAll();
+ } else if (level >= TRIM_MEMORY_BACKGROUND) {
+ mLruCache.trimToSize(mLruCache.size() / 2);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLowMemory() {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onConfigurationChanged(final Configuration newConfig) {
+ // Nothing to do
+ }
+ });
+ }
+
+ /**
+ * Find and return an existing ImageCache stored in a {@link RetainFragment}
+ * , if not found a new one is created using the supplied params and saved
+ * to a {@link RetainFragment}
+ *
+ * @param activity The calling {@link FragmentActivity}
+ * @return An existing retained ImageCache object or a new one if one did
+ * not exist
+ */
+ public static final ImageCache findOrCreateCache(final Activity activity) {
+
+ // Search for, or create an instance of the non-UI RetainFragment
+ final RetainFragment retainFragment = findOrCreateRetainFragment(
+ activity.getFragmentManager());
+
+ // See if we already have an ImageCache stored in RetainFragment
+ ImageCache cache = (ImageCache)retainFragment.getObject();
+
+ // No existing ImageCache, create one and store it in RetainFragment
+ if (cache == null) {
+ cache = getInstance(activity);
+ retainFragment.setObject(cache);
+ }
+ return cache;
+ }
+
+ /**
+ * Locate an existing instance of this {@link Fragment} or if not found,
+ * create and add it using {@link FragmentManager}
+ *
+ * @param fm The {@link FragmentManager} to use
+ * @return The existing instance of the {@link Fragment} or the new instance
+ * if just created
+ */
+ public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
+ // Check to see if we have retained the worker fragment
+ RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
+
+ // If not retained, we need to create and add it
+ if (retainFragment == null) {
+ retainFragment = new RetainFragment();
+ fm.beginTransaction().add(retainFragment, TAG).commit();
+ }
+ return retainFragment;
+ }
+
+ /**
+ * Adds a new image to the memory and disk caches
+ *
+ * @param data The key used to store the image
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToCache(final String data, final Bitmap bitmap) {
+ if (data == null || bitmap == null) {
+ return;
+ }
+
+ // Add to memory cache
+ addBitmapToMemCache(data, bitmap);
+
+ // Add to disk cache
+ if (mDiskCache != null) {
+ final String key = hashKeyForDisk(data);
+ OutputStream out = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ if (snapshot == null) {
+ final DiskLruCache.Editor editor = mDiskCache.edit(key);
+ if (editor != null) {
+ out = editor.newOutputStream(DISK_CACHE_INDEX);
+ bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
+ editor.commit();
+ out.close();
+ flush();
+ }
+ } else {
+ snapshot.getInputStream(DISK_CACHE_INDEX).close();
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ out = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } catch (final IllegalStateException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called to add a new image to the memory cache
+ *
+ * @param data The key identifier
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
+ if (data == null || bitmap == null) {
+ return;
+ }
+ // Add to memory cache
+ if (getBitmapFromMemCache(data) == null) {
+ mLruCache.put(data, bitmap);
+ }
+ }
+
+ /**
+ * Fetches a cached image from the memory cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromMemCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+ if (mLruCache != null) {
+ final Bitmap lruBitmap = mLruCache.get(data);
+ if (lruBitmap != null) {
+ return lruBitmap;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetches a cached image from the disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromDiskCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+
+ // Check in the memory cache here to avoid going to the disk cache less
+ // often
+ if (getBitmapFromMemCache(data) != null) {
+ return getBitmapFromMemCache(data);
+ }
+
+ waitUntilUnpaused();
+ final String key = hashKeyForDisk(data);
+ if (mDiskCache != null) {
+ InputStream inputStream = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ if (snapshot != null) {
+ inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
+ if (inputStream != null) {
+ final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "getBitmapFromDiskCache - " + e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (final IOException e) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return a cached image from memory cache before fetching from the
+ * disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getCachedBitmap(final String data) {
+ if (data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getBitmapFromMemCache(data);
+ if (cachedImage == null) {
+ cachedImage = getBitmapFromDiskCache(data);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return the album art from memory cache and disk cache, before
+ * calling {@code #getArtworkFromFile(Context, String)} again
+ *
+ * @param context The {@link Context} to use
+ * @param data The name of the album art
+ * @param id The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
+ if (context == null || data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getCachedBitmap(data);
+ if (cachedImage == null && id >= 0) {
+ cachedImage = getArtworkFromFile(context, id);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Used to fetch the artwork for an album locally from the user's device
+ *
+ * @param context The {@link Context} to use
+ * @param albumID The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
+ if (albumId < 0) {
+ return null;
+ }
+ Bitmap artwork = null;
+ waitUntilUnpaused();
+ try {
+ final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
+ final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
+ .openFileDescriptor(uri, "r");
+ if (parcelFileDescriptor != null) {
+ final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
+ }
+ } catch (final IllegalStateException e) {
+ // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
+ } catch (final FileNotFoundException e) {
+ // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
+ } catch (final OutOfMemoryError evict) {
+ // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
+ evictAll();
+ }
+ return artwork;
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.flush();
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "flush - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Clear the disk cache
+ try {
+ if (mDiskCache != null) {
+ mDiskCache.delete();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "clearCaches - " + e);
+ }
+ // Clear the memory cache
+ evictAll();
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.close();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "close - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Evicts all of the items from the memory cache and lets the system know
+ * now would be a good time to garbage collect
+ */
+ public void evictAll() {
+ if (mLruCache != null) {
+ mLruCache.evictAll();
+ }
+ System.gc();
+ }
+
+ /**
+ * @param key The key used to identify which cache entries to delete.
+ */
+ public void removeFromCache(final String key) {
+ if (key == null) {
+ return;
+ }
+ // Remove the Lru entry
+ if (mLruCache != null) {
+ mLruCache.remove(key);
+ }
+
+ try {
+ // Remove the disk entry
+ if (mDiskCache != null) {
+ mDiskCache.remove(hashKeyForDisk(key));
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "remove - " + e);
+ }
+ flush();
+ }
+
+ /**
+ * Used to temporarily pause the disk cache while the user is scrolling to
+ * improve scrolling.
+ *
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ synchronized (mPauseLock) {
+ if (mPauseDiskAccess != pause) {
+ mPauseDiskAccess = pause;
+ if (!pause) {
+ mPauseLock.notify();
+ }
+ }
+ }
+ }
+
+ private void waitUntilUnpaused() {
+ synchronized (mPauseLock) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ while (mPauseDiskAccess) {
+ try {
+ mPauseLock.wait();
+ } catch (InterruptedException e) {
+ // ignored, we'll start waiting again
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @return True if the user is scrolling, false otherwise.
+ */
+ public boolean isDiskCachePaused() {
+ return mPauseDiskAccess;
+ }
+
+ /**
+ * Get a usable cache directory (external if available, internal otherwise)
+ *
+ * @param context The {@link Context} to use
+ * @param uniqueName A unique directory name to append to the cache
+ * directory
+ * @return The cache directory
+ */
+ public static final File getDiskCacheDir(final Context context, final String uniqueName) {
+ // getExternalCacheDir(context) returns null if external storage is not ready
+ final String cachePath = getExternalCacheDir(context) != null
+ ? getExternalCacheDir(context).getPath()
+ : context.getCacheDir().getPath();
+ return new File(cachePath, uniqueName);
+ }
+
+ /**
+ * Check if external storage is built-in or removable
+ *
+ * @return True if external storage is removable (like an SD card), false
+ * otherwise
+ */
+ public static final boolean isExternalStorageRemovable() {
+ return Environment.isExternalStorageRemovable();
+ }
+
+ /**
+ * Get the external app cache directory
+ *
+ * @param context The {@link Context} to use
+ * @return The external cache directory
+ */
+ public static final File getExternalCacheDir(final Context context) {
+ return context.getExternalCacheDir();
+ }
+
+ /**
+ * Check how much usable space is available at a given path.
+ *
+ * @param path The path to check
+ * @return The space available in bytes
+ */
+ public static final long getUsableSpace(final File path) {
+ return path.getUsableSpace();
+ }
+
+ /**
+ * A hashing method that changes a string (like a URL) into a hash suitable
+ * for using as a disk filename.
+ *
+ * @param key The key used to store the file
+ */
+ public static final String hashKeyForDisk(final String key) {
+ String cacheKey;
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("MD5");
+ digest.update(key.getBytes());
+ cacheKey = bytesToHexString(digest.digest());
+ } catch (final NoSuchAlgorithmException e) {
+ cacheKey = String.valueOf(key.hashCode());
+ }
+ return cacheKey;
+ }
+
+ /**
+ * http://stackoverflow.com/questions/332079
+ *
+ * @param bytes The bytes to convert.
+ * @return A {@link String} converted from the bytes of a hashable key used
+ * to store a filename on the disk, to hex digits.
+ */
+ private static final String bytesToHexString(final byte[] bytes) {
+ final StringBuilder builder = new StringBuilder();
+ for (final byte b : bytes) {
+ final String hex = Integer.toHexString(0xFF & b);
+ if (hex.length() == 1) {
+ builder.append('0');
+ }
+ builder.append(hex);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A simple non-UI Fragment that stores a single Object and is retained over
+ * configuration changes. In this sample it will be used to retain an
+ * {@link ImageCache} object.
+ */
+ public static final class RetainFragment extends Fragment {
+
+ /**
+ * The object to be stored
+ */
+ private Object mObject;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public RetainFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Make sure this Fragment is retained over a configuration change
+ setRetainInstance(true);
+ }
+
+ /**
+ * Store a single object in this {@link Fragment}
+ *
+ * @param object The object to store
+ */
+ public void setObject(final Object object) {
+ mObject = object;
+ }
+
+ /**
+ * Get the stored object
+ *
+ * @return The stored object
+ */
+ public Object getObject() {
+ return mObject;
+ }
+ }
+
+ /**
+ * Used to cache images via {@link LruCache}.
+ */
+ public static final class MemoryCache extends LruCache<String, Bitmap> {
+
+ /**
+ * Constructor of <code>MemoryCache</code>
+ *
+ * @param maxSize The allowed size of the {@link LruCache}
+ */
+ public MemoryCache(final int maxSize) {
+ super(maxSize);
+ }
+
+ /**
+ * Get the size in bytes of a bitmap.
+ */
+ public static final int getBitmapSize(final Bitmap bitmap) {
+ return bitmap.getByteCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
+ return getBitmapSize(paramBitmap);
+ }
+
+ }
+
+}
diff --git a/src/com/cyngn/eleven/cache/ImageFetcher.java b/src/com/cyngn/eleven/cache/ImageFetcher.java
new file mode 100644
index 0000000..0d7c7dc
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/ImageFetcher.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.lastfm.Album;
+import com.cyngn.eleven.lastfm.Artist;
+import com.cyngn.eleven.lastfm.MusicEntry;
+import com.cyngn.eleven.lastfm.ImageSize;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * A subclass of {@link ImageWorker} that fetches images from a URL.
+ */
+public class ImageFetcher extends ImageWorker {
+
+ public static final int IO_BUFFER_SIZE_BYTES = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
+
+ private static ImageFetcher sInstance = null;
+
+ /**
+ * Creates a new instance of {@link ImageFetcher}.
+ *
+ * @param context The {@link Context} to use.
+ */
+ public ImageFetcher(final Context context) {
+ super(context);
+ }
+
+ /**
+ * Used to create a singleton of the image fetcher
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static final ImageFetcher getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageFetcher(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected Bitmap processBitmap(final String url) {
+ if (url == null) {
+ return null;
+ }
+ final File file = downloadBitmapToFile(mContext, url, DEFAULT_HTTP_CACHE_DIR);
+ if (file != null) {
+ // Return a sampled down version
+ final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
+ file.delete();
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ return null;
+ }
+
+ private static String getBestImage(MusicEntry e) {
+ final ImageSize[] QUALITY = {ImageSize.EXTRALARGE, ImageSize.LARGE, ImageSize.MEDIUM,
+ ImageSize.SMALL, ImageSize.UNKNOWN};
+ for(ImageSize q : QUALITY) {
+ String url = e.getImageURL(q);
+ if (url != null) {
+ return url;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String processImageUrl(final String artistName, final String albumName,
+ final ImageType imageType) {
+ switch (imageType) {
+ case ARTIST:
+ if (!TextUtils.isEmpty(artistName)) {
+ if (PreferenceUtils.getInstance(mContext).downloadMissingArtistImages()) {
+ final Artist artist = Artist.getInfo(mContext, artistName);
+ if (artist != null) {
+ return getBestImage(artist);
+ }
+ }
+ }
+ break;
+ case ALBUM:
+ if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
+ if (PreferenceUtils.getInstance(mContext).downloadMissingArtwork()) {
+ final Artist correction = Artist.getCorrection(mContext, artistName);
+ if (correction != null) {
+ final Album album = Album.getInfo(mContext, correction.getName(),
+ albumName);
+ if (album != null) {
+ return getBestImage(album);
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Used to fetch album images.
+ */
+ public void loadAlbumImage(final String artistName, final String albumName, final long albumId,
+ final ImageView imageView) {
+ loadImage(generateAlbumCacheKey(albumName, artistName), artistName, albumName, albumId, imageView,
+ ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch the current artwork.
+ */
+ public void loadCurrentArtwork(final ImageView imageView) {
+ loadImage(generateAlbumCacheKey(MusicUtils.getAlbumName(), MusicUtils.getArtistName()),
+ MusicUtils.getArtistName(), MusicUtils.getAlbumName(), MusicUtils.getCurrentAlbumId(),
+ imageView, ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch artist images.
+ */
+ public void loadArtistImage(final String key, final ImageView imageView) {
+ loadImage(key, key, null, -1, imageView, ImageType.ARTIST);
+ }
+
+ /**
+ * Used to fetch the current artist image.
+ */
+ public void loadCurrentArtistImage(final ImageView imageView) {
+ loadImage(MusicUtils.getArtistName(), MusicUtils.getArtistName(), null, -1, imageView,
+ ImageType.ARTIST);
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageCache != null) {
+ mImageCache.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ if (mImageCache != null) {
+ mImageCache.clearCaches();
+ }
+ }
+
+ /**
+ * @param key The key used to find the image to remove
+ */
+ public void removeFromCache(final String key) {
+ if (mImageCache != null) {
+ mImageCache.removeFromCache(key);
+ }
+ }
+
+ /**
+ * @param key The key used to find the image to return
+ */
+ public Bitmap getCachedBitmap(final String key) {
+ if (mImageCache != null) {
+ return mImageCache.getCachedBitmap(key);
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * @param keyAlbum The key (album name) used to find the album art to return
+ * @param keyArtist The key (artist name) used to find the album art to return
+ */
+ public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist) {
+ return getCachedArtwork(keyAlbum, keyArtist,
+ MusicUtils.getIdForAlbum(mContext, keyAlbum, keyArtist));
+ }
+
+ /**
+ * @param keyAlbum The key (album name) used to find the album art to return
+ * @param keyArtist The key (artist name) used to find the album art to return
+ * @param keyId The key (album id) used to find the album art to return
+ */
+ public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist,
+ final long keyId) {
+ if (mImageCache != null) {
+ return mImageCache.getCachedArtwork(mContext,
+ generateAlbumCacheKey(keyAlbum, keyArtist),
+ keyId);
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * Finds cached or downloads album art. Used in {@link MusicPlaybackService}
+ * to set the current album art in the notification and lock screen
+ *
+ * @param albumName The name of the current album
+ * @param albumId The ID of the current album
+ * @param artistName The album artist in case we should have to download
+ * missing artwork
+ * @return The album art as an {@link Bitmap}
+ */
+ public Bitmap getArtwork(final String albumName, final long albumId, final String artistName) {
+ // Check the disk cache
+ Bitmap artwork = null;
+
+ if (artwork == null && albumName != null && mImageCache != null) {
+ artwork = mImageCache.getBitmapFromDiskCache(
+ generateAlbumCacheKey(albumName, artistName));
+ }
+ if (artwork == null && albumId >= 0 && mImageCache != null) {
+ // Check for local artwork
+ artwork = mImageCache.getArtworkFromFile(mContext, albumId);
+ }
+ if (artwork != null) {
+ return artwork;
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * Download a {@link Bitmap} from a URL, write it to a disk and return the
+ * File pointer. This implementation uses a simple disk cache.
+ *
+ * @param context The context to use
+ * @param urlString The URL to fetch
+ * @return A {@link File} pointing to the fetched bitmap
+ */
+ public static final File downloadBitmapToFile(final Context context, final String urlString,
+ final String uniqueName) {
+ final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
+
+ if (!cacheDir.exists()) {
+ cacheDir.mkdir();
+ }
+
+ HttpURLConnection urlConnection = null;
+ BufferedOutputStream out = null;
+
+ try {
+ final File tempFile = File.createTempFile("bitmap", null, cacheDir); //$NON-NLS-1$
+
+ final URL url = new URL(urlString);
+ urlConnection = (HttpURLConnection)url.openConnection();
+ if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ return null;
+ }
+ final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
+ IO_BUFFER_SIZE_BYTES);
+ out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
+
+ int oneByte;
+ while ((oneByte = in.read()) != -1) {
+ out.write(oneByte);
+ }
+ return tempFile;
+ } catch (final IOException ignored) {
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decode and sample down a {@link Bitmap} from a file to the requested
+ * width and height.
+ *
+ * @param filename The full path of the file to decode
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return A {@link Bitmap} sampled down from the original with the same
+ * aspect ratio and dimensions that are equal to or greater than the
+ * requested width and height
+ */
+ public static Bitmap decodeSampledBitmapFromFile(final String filename) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filename, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
+ DEFAULT_MAX_IMAGE_HEIGHT);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(filename, options);
+ }
+
+ /**
+ * Calculate an inSampleSize for use in a
+ * {@link android.graphics.BitmapFactory.Options} object when decoding
+ * bitmaps using the decode* methods from {@link BitmapFactory}. This
+ * implementation calculates the closest inSampleSize that will result in
+ * the final decoded bitmap having a width and height equal to or larger
+ * than the requested width and height. This implementation does not ensure
+ * a power of 2 is returned for inSampleSize which can be faster when
+ * decoding but results in a larger bitmap which isn't as useful for caching
+ * purposes.
+ *
+ * @param options An options object with out* params already populated (run
+ * through a decode* method with inJustDecodeBounds==true
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return The value to be used for inSampleSize
+ */
+ public static final int calculateInSampleSize(final BitmapFactory.Options options,
+ final int reqWidth, final int reqHeight) {
+ /* Raw height and width of image */
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+
+ // This offers some additional logic in case the image has a strange
+ // aspect ratio. For example, a panorama may have a much larger
+ // width than height. In these cases the total pixels might still
+ // end up being too large to fit comfortably in memory, so we should
+ // be more aggressive with sample down the image (=larger
+ // inSampleSize).
+
+ final float totalPixels = width * height;
+
+ /* More than 2x the requested pixels we'll sample down further */
+ final float totalReqPixelsCap = reqWidth * reqHeight * 2;
+
+ while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
+ inSampleSize++;
+ }
+ }
+ return inSampleSize;
+ }
+
+ /**
+ * Generates key used by album art cache. It needs both album name and artist name
+ * to let to select correct image for the case when there are two albums with the
+ * same artist.
+ *
+ * @param albumName The album name the cache key needs to be generated.
+ * @param artistName The artist name the cache key needs to be generated.
+ * @return
+ */
+ public static String generateAlbumCacheKey(final String albumName, final String artistName) {
+ if (albumName == null || artistName == null) {
+ return null;
+ }
+ return new StringBuilder(albumName)
+ .append("_")
+ .append(artistName)
+ .append("_")
+ .append(Config.ALBUM_ART_SUFFIX)
+ .toString();
+ }
+}
diff --git a/src/com/cyngn/eleven/cache/ImageWorker.java b/src/com/cyngn/eleven/cache/ImageWorker.java
new file mode 100644
index 0000000..fa40bba
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/ImageWorker.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.AsyncTask;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * This class wraps up completing some arbitrary long running work when loading
+ * a {@link Bitmap} to an {@link ImageView}. It handles things like using a
+ * memory and disk cache, running the work in a background thread and setting a
+ * placeholder image.
+ */
+public abstract class ImageWorker {
+
+ /**
+ * Default transition drawable fade time
+ */
+ private static final int FADE_IN_TIME = 200;
+
+ /**
+ * Default artwork
+ */
+ private final BitmapDrawable mDefaultArtwork;
+
+ /**
+ * The resources to use
+ */
+ private final Resources mResources;
+
+ /**
+ * First layer of the transition drawable
+ */
+ private final ColorDrawable mCurrentDrawable;
+
+ /**
+ * Layer drawable used to cross fade the result from the worker
+ */
+ private final Drawable[] mArrayDrawable;
+
+ /**
+ * Default album art
+ */
+ private final Bitmap mDefault;
+
+ /**
+ * The Context to use
+ */
+ protected Context mContext;
+
+ /**
+ * Disk and memory caches
+ */
+ protected ImageCache mImageCache;
+
+ /**
+ * Constructor of <code>ImageWorker</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ protected ImageWorker(final Context context) {
+ mContext = context.getApplicationContext();
+ mResources = mContext.getResources();
+ // Create the default artwork
+ final ThemeUtils theme = new ThemeUtils(context);
+ mDefault = ((BitmapDrawable) theme.getDrawable("default_artwork")).getBitmap();
+ mDefaultArtwork = new BitmapDrawable(mResources, mDefault);
+ // No filter and no dither makes things much quicker
+ mDefaultArtwork.setFilterBitmap(false);
+ mDefaultArtwork.setDither(false);
+ // Create the transparent layer for the transition drawable
+ mCurrentDrawable = new ColorDrawable(mResources.getColor(R.color.transparent));
+ // A transparent image (layer 0) and the new result (layer 1)
+ mArrayDrawable = new Drawable[2];
+ mArrayDrawable[0] = mCurrentDrawable;
+ // XXX The second layer is set in the worker task.
+ }
+
+ /**
+ * Set the {@link ImageCache} object to use with this ImageWorker.
+ *
+ * @param cacheCallback new {@link ImageCache} object.
+ */
+ public void setImageCache(final ImageCache cacheCallback) {
+ mImageCache = cacheCallback;
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ if (mImageCache != null) {
+ mImageCache.close();
+ }
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ if (mImageCache != null) {
+ mImageCache.flush();
+ }
+ }
+
+ /**
+ * Adds a new image to the memory and disk caches
+ *
+ * @param data The key used to store the image
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToCache(final String key, final Bitmap bitmap) {
+ if (mImageCache != null) {
+ mImageCache.addBitmapToCache(key, bitmap);
+ }
+ }
+
+ /**
+ * @return The deafult artwork
+ */
+ public Bitmap getDefaultArtwork() {
+ return mDefault;
+ }
+
+ /**
+ * The actual {@link AsyncTask} that will process the image.
+ */
+ private final class BitmapWorkerTask extends AsyncTask<String, Void, TransitionDrawable> {
+
+ /**
+ * The {@link ImageView} used to set the result
+ */
+ private final WeakReference<ImageView> mImageReference;
+
+ /**
+ * Type of URL to download
+ */
+ private final ImageType mImageType;
+
+ /**
+ * The key used to store cached entries
+ */
+ private String mKey;
+
+ /**
+ * Artist name param
+ */
+ private String mArtistName;
+
+ /**
+ * Album name parm
+ */
+ private String mAlbumName;
+
+ /**
+ * The album ID used to find the corresponding artwork
+ */
+ private long mAlbumId;
+
+ /**
+ * The URL of an image to download
+ */
+ private String mUrl;
+
+ /**
+ * Constructor of <code>BitmapWorkerTask</code>
+ *
+ * @param imageView The {@link ImageView} to use.
+ * @param imageType The type of image URL to fetch for.
+ */
+ @SuppressWarnings("deprecation")
+ public BitmapWorkerTask(final ImageView imageView, final ImageType imageType) {
+ imageView.setBackgroundDrawable(mDefaultArtwork);
+ mImageReference = new WeakReference<ImageView>(imageView);
+ mImageType = imageType;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected TransitionDrawable doInBackground(final String... params) {
+ // Define the key
+ mKey = params[0];
+
+ // The result
+ Bitmap bitmap = null;
+
+ // First, check the disk cache for the image
+ if (mKey != null && mImageCache != null && !isCancelled()
+ && getAttachedImageView() != null) {
+ bitmap = mImageCache.getCachedBitmap(mKey);
+ }
+
+ // Define the album id now
+ mAlbumId = Long.valueOf(params[3]);
+
+ // Second, if we're fetching artwork, check the device for the image
+ if (bitmap == null && mImageType.equals(ImageType.ALBUM) && mAlbumId >= 0
+ && mKey != null && !isCancelled() && getAttachedImageView() != null
+ && mImageCache != null) {
+ bitmap = mImageCache.getCachedArtwork(mContext, mKey, mAlbumId);
+ }
+
+ // Third, by now we need to download the image
+ if (bitmap == null && ApolloUtils.isOnline(mContext) && !isCancelled()
+ && getAttachedImageView() != null) {
+ // Now define what the artist name, album name, and url are.
+ mArtistName = params[1];
+ mAlbumName = params[2];
+ mUrl = processImageUrl(mArtistName, mAlbumName, mImageType);
+ if (mUrl != null) {
+ bitmap = processBitmap(mUrl);
+ }
+ }
+
+ // Fourth, add the new image to the cache
+ if (bitmap != null && mKey != null && mImageCache != null) {
+ addBitmapToCache(mKey, bitmap);
+ }
+
+ // Add the second layer to the transiation drawable
+ if (bitmap != null) {
+ final BitmapDrawable layerTwo = new BitmapDrawable(mResources, bitmap);
+ layerTwo.setFilterBitmap(false);
+ layerTwo.setDither(false);
+ mArrayDrawable[1] = layerTwo;
+
+ // Finally, return the image
+ final TransitionDrawable result = new TransitionDrawable(mArrayDrawable);
+ result.setCrossFadeEnabled(true);
+ result.startTransition(FADE_IN_TIME);
+ return result;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable result) {
+ if (isCancelled()) {
+ result = null;
+ }
+ final ImageView imageView = getAttachedImageView();
+ if (result != null && imageView != null) {
+ imageView.setImageDrawable(result);
+ }
+ }
+
+ /**
+ * @return The {@link ImageView} associated with this task as long as
+ * the ImageView's task still points to this task as well.
+ * Returns null otherwise.
+ */
+ private final ImageView getAttachedImageView() {
+ final ImageView imageView = mImageReference.get();
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (this == bitmapWorkerTask) {
+ return imageView;
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Calls {@code cancel()} in the worker task
+ *
+ * @param imageView the {@link ImageView} to use
+ */
+ public static final void cancelWork(final ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (bitmapWorkerTask != null) {
+ bitmapWorkerTask.cancel(true);
+ }
+ }
+
+ /**
+ * Returns true if the current work has been canceled or if there was no
+ * work in progress on this image view. Returns false if the work in
+ * progress deals with the same data. The work is not stopped in that case.
+ */
+ public static final boolean executePotentialWork(final Object data, final ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (bitmapWorkerTask != null) {
+ final Object bitmapData = bitmapWorkerTask.mKey;
+ if (bitmapData == null || !bitmapData.equals(data)) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ // The same work is already in progress
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Used to determine if the current image drawable has an instance of
+ * {@link BitmapWorkerTask}
+ *
+ * @param imageView Any {@link ImageView}.
+ * @return Retrieve the currently active work task (if any) associated with
+ * this {@link ImageView}. null if there is no such task.
+ */
+ private static final BitmapWorkerTask getBitmapWorkerTask(final ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable)drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * A custom {@link BitmapDrawable} that will be attached to the
+ * {@link ImageView} while the work is in progress. Contains a reference to
+ * the actual worker task, so that it can be stopped if a new binding is
+ * required, and makes sure that only the last started worker process can
+ * bind its result, independently of the finish order.
+ */
+ private static final class AsyncDrawable extends ColorDrawable {
+
+ private final WeakReference<BitmapWorkerTask> mBitmapWorkerTaskReference;
+
+ /**
+ * Constructor of <code>AsyncDrawable</code>
+ */
+ public AsyncDrawable(final Resources res, final Bitmap bitmap,
+ final BitmapWorkerTask mBitmapWorkerTask) {
+ super(Color.TRANSPARENT);
+ mBitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(mBitmapWorkerTask);
+ }
+
+ /**
+ * @return The {@link BitmapWorkerTask} associated with this drawable
+ */
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return mBitmapWorkerTaskReference.get();
+ }
+ }
+
+ /**
+ * Called to fetch the artist or ablum art.
+ *
+ * @param key The unique identifier for the image.
+ * @param artistName The artist name for the Last.fm API.
+ * @param albumName The album name for the Last.fm API.
+ * @param albumId The album art index, to check for missing artwork.
+ * @param imageView The {@link ImageView} used to set the cached
+ * {@link Bitmap}.
+ * @param imageType The type of image URL to fetch for.
+ */
+ protected void loadImage(final String key, final String artistName, final String albumName,
+ final long albumId, final ImageView imageView, final ImageType imageType) {
+ if (key == null || mImageCache == null || imageView == null) {
+ return;
+ }
+ // First, check the memory for the image
+ final Bitmap lruBitmap = mImageCache.getBitmapFromMemCache(key);
+ if (lruBitmap != null && imageView != null) {
+ // Bitmap found in memory cache
+ imageView.setImageBitmap(lruBitmap);
+ } else if (executePotentialWork(key, imageView)
+ && imageView != null && !mImageCache.isDiskCachePaused()) {
+ // Otherwise run the worker task
+ final BitmapWorkerTask bitmapWorkerTask = new BitmapWorkerTask(imageView, imageType);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mDefault,
+ bitmapWorkerTask);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ ApolloUtils.execute(false, bitmapWorkerTask, key,
+ artistName, albumName, String.valueOf(albumId));
+ } catch (RejectedExecutionException e) {
+ // Executor has exhausted queue space, show default artwork
+ imageView.setImageBitmap(getDefaultArtwork());
+ }
+ }
+ }
+
+ /**
+ * Subclasses should override this to define any processing or work that
+ * must happen to produce the final {@link Bitmap}. This will be executed in
+ * a background thread and be long running.
+ *
+ * @param key The key to identify which image to process, as provided by
+ * {@link ImageWorker#loadImage(mKey, ImageView)}
+ * @return The processed {@link Bitmap}.
+ */
+ protected abstract Bitmap processBitmap(String key);
+
+ /**
+ * Subclasses should override this to define any processing or work that
+ * must happen to produce the URL needed to fetch the final {@link Bitmap}.
+ *
+ * @param artistName The artist name param used in the Last.fm API.
+ * @param albumName The album name param used in the Last.fm API.
+ * @param imageType The type of image URL to fetch for.
+ * @return The image URL for an artist image or album image.
+ */
+ protected abstract String processImageUrl(String artistName, String albumName,
+ ImageType imageType);
+
+ /**
+ * Used to define what type of image URL to fetch for, artist or album.
+ */
+ public enum ImageType {
+ ARTIST, ALBUM;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/cache/LruCache.java b/src/com/cyngn/eleven/cache/LruCache.java
new file mode 100644
index 0000000..bc7debf
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/LruCache.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.cache;
+
+// NOTE: upstream of this class is android.util.LruCache, changes below
+// expose trimToSize() to be called externally.
+
+import android.annotation.SuppressLint;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Static library version of {@link android.util.LruCache}. Used to write apps
+ * that run on API levels prior to 12. When running on API level 12 or above,
+ * this implementation is still used; it does not try to switch to the
+ * framework's implementation. See the framework SDK documentation for a class
+ * overview.
+ */
+public class LruCache<K, V> {
+
+ private final LinkedHashMap<K, V> map;
+
+ private final int maxSize;
+
+ /** Size of this cache in units. Not necessarily the number of elements. */
+ private int size;
+
+ private int putCount;
+
+ private int createCount;
+
+ private int evictionCount;
+
+ private int hitCount;
+
+ private int missCount;
+
+ /**
+ * @param maxSize for caches that do not override {@link #sizeOf}, this is
+ * the maximum number of entries in the cache. For all other
+ * caches, this is the maximum sum of the sizes of the entries in
+ * this cache.
+ */
+ public LruCache(final int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+ }
+
+ /**
+ * Returns the value for {@code key} if it exists in the cache or can be
+ * created by {@code #create}. If a value was returned, it is moved to the
+ * head of the queue. This returns null if a value is not cached and cannot
+ * be created.
+ */
+ public final V get(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ this.hitCount++;
+ return mapValue;
+ }
+ this.missCount++;
+ }
+
+ /*
+ * Attempt to create a value. This may take a long time, and the map may
+ * be different when create() returns. If a conflicting value was added
+ * to the map while create() was working, we leave that value in the map
+ * and release the created value.
+ */
+
+ final V createdValue = create(key);
+ if (createdValue == null) {
+ return null;
+ }
+
+ synchronized (this) {
+ this.createCount++;
+ mapValue = map.put(key, createdValue);
+
+ if (mapValue != null) {
+ /* There was a conflict so undo that last put */
+ this.map.put(key, mapValue);
+ } else {
+ this.size += safeSizeOf(key, createdValue);
+ }
+ }
+
+ if (mapValue != null) {
+ entryRemoved(false, key, createdValue, mapValue);
+ return mapValue;
+ } else {
+ trimToSize(maxSize);
+ return createdValue;
+ }
+ }
+
+ /**
+ * Caches {@code value} for {@code key}. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V put(final K key, final V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException("key == null || value == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ this.putCount++;
+ this.size += safeSizeOf(key, value);
+ previous = this.map.put(key, value);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, value);
+ }
+
+ trimToSize(maxSize);
+ return previous;
+ }
+
+ /**
+ * @param maxSize the maximum size of the cache before returning. May be -1
+ * to evict even 0-sized elements.
+ */
+ public void trimToSize(final int maxSize) {
+ while (true) {
+ K key;
+ V value;
+ synchronized (this) {
+ if (this.size < 0 || this.map.isEmpty() && size != 0) {
+ throw new IllegalStateException(getClass().getName()
+ + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (this.size <= maxSize || this.map.isEmpty()) {
+ break;
+ }
+
+ final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ this.map.remove(key);
+ this.size -= safeSizeOf(key, value);
+ this.evictionCount++;
+ }
+
+ entryRemoved(true, key, value, null);
+ }
+ }
+
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ previous = this.map.remove(key);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false if
+ * the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put}. Otherwise it was
+ * caused by an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
+ final V newValue) {
+ }
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ * <p>
+ * If a value for {@code key} exists in the cache when this method returns,
+ * the created value will be released with {@link #entryRemoved} and
+ * discarded. This can occur when multiple threads request the same key at
+ * the same time (causing multiple values to be created), or when one thread
+ * calls {@link #put} while another is creating a value for the same key.
+ */
+ protected V create(final K key) {
+ return null;
+ }
+
+ private int safeSizeOf(final K key, final V value) {
+ final int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size is
+ * the number of entries and max size is the maximum number of entries.
+ * <p>
+ * An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(final K key, final V value) {
+ return 1;
+ }
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return this.size;
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return this.maxSize;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned a value.
+ */
+ public synchronized final int hitCount() {
+ return this.hitCount;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return this.missCount;
+ }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return this.createCount;
+ }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return this.putCount;
+ }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return this.evictionCount;
+ }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map<K, V> snapshot() {
+ return new LinkedHashMap<K, V>(this.map);
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public synchronized final String toString() {
+ final int accesses = this.hitCount + this.missCount;
+ final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
+ return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
+ this.hitCount, this.missCount, hitPercent);
+ }
+}
diff --git a/src/com/cyngn/eleven/dragdrop/DragSortController.java b/src/com/cyngn/eleven/dragdrop/DragSortController.java
new file mode 100644
index 0000000..c14e6b1
--- /dev/null
+++ b/src/com/cyngn/eleven/dragdrop/DragSortController.java
@@ -0,0 +1,442 @@
+
+package com.cyngn.eleven.dragdrop;
+
+import android.graphics.Point;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.AdapterView;
+
+/**
+ * Class that starts and stops item drags on a {@link DragSortListView} based on
+ * touch gestures. This class also inherits from {@link SimpleFloatViewManager},
+ * which provides basic float View creation. An instance of this class is meant
+ * to be passed to the methods {@link DragSortListView#setTouchListener()} and
+ * {@link DragSortListView#setFloatViewManager()} of your
+ * {@link DragSortListView} instance.
+ */
+public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener,
+ GestureDetector.OnGestureListener {
+
+ public final static int ON_DOWN = 0;
+
+ public final static int ON_DRAG = 1;
+
+ public final static int ON_LONG_PRESS = 2;
+
+ public final static int FLING_RIGHT_REMOVE = 0;
+
+ public final static int FLING_LEFT_REMOVE = 1;
+
+ public final static int SLIDE_RIGHT_REMOVE = 2;
+
+ public final static int SLIDE_LEFT_REMOVE = 3;
+
+ public final static int MISS = -1;
+
+ private final GestureDetector mDetector;
+
+ private final GestureDetector mFlingRemoveDetector;
+
+ private final int mTouchSlop;
+
+ private final int[] mTempLoc = new int[2];
+
+ private final float mFlingSpeed = 500f;
+
+ private final DragSortListView mDslv;
+
+ private boolean mSortEnabled = true;
+
+ private boolean mRemoveEnabled = false;
+
+ private boolean mDragging = false;
+
+ private int mDragInitMode = ON_DOWN;
+
+ private int mRemoveMode;
+
+ private int mHitPos = MISS;
+
+ private int mItemX;
+
+ private int mItemY;
+
+ private int mCurrX;
+
+ private int mCurrY;
+
+ private int mDragHandleId;
+
+ private float mOrigFloatAlpha = 1.0f;
+
+ /**
+ * Calls {@link #DragSortController(DragSortListView, int)} with a 0 drag
+ * handle id, FLING_RIGHT_REMOVE remove mode, and ON_DOWN drag init. By
+ * default, sorting is enabled, and removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ */
+ public DragSortController(DragSortListView dslv) {
+ this(dslv, 0, ON_DOWN, FLING_RIGHT_REMOVE);
+ }
+
+ /**
+ * By default, sorting is enabled, and removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ * @param dragHandleId The resource id of the View that represents the drag
+ * handle in a list item.
+ */
+ public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
+ int removeMode) {
+ super(dslv);
+ mDslv = dslv;
+ mDetector = new GestureDetector(dslv.getContext(), this);
+ mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
+ mFlingRemoveDetector.setIsLongpressEnabled(false);
+ mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
+ mDragHandleId = dragHandleId;
+ setRemoveMode(removeMode);
+ setDragInitMode(dragInitMode);
+ mOrigFloatAlpha = dslv.getFloatAlpha();
+ }
+
+ /**
+ * @return The current drag init mode.
+ */
+ public int getDragInitMode() {
+ return mDragInitMode;
+ }
+
+ /**
+ * Set how a drag is initiated. Needs to be one of {@link ON_DOWN},
+ * {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
+ *
+ * @param mode The drag init mode.
+ */
+ public void setDragInitMode(int mode) {
+ mDragInitMode = mode;
+ }
+
+ /**
+ * Enable/Disable list item sorting. Disabling is useful if only item
+ * removal is desired. Prevents drags in the vertical direction.
+ *
+ * @param enabled Set <code>true</code> to enable list item sorting.
+ */
+ public void setSortEnabled(boolean enabled) {
+ mSortEnabled = enabled;
+ }
+
+ /**
+ * @return True if sort is enabled, false otherwise.
+ */
+ public boolean isSortEnabled() {
+ return mSortEnabled;
+ }
+
+ /**
+ * One of {@link FLING_RIGHT_REMOVE}, {@link FLING_LEFT_REMOVE},
+ * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
+ */
+ public void setRemoveMode(int mode) {
+ mRemoveMode = mode;
+ }
+
+ /**
+ * @return The current remove mode.
+ */
+ public int getRemoveMode() {
+ return mRemoveMode;
+ }
+
+ /**
+ * Enable/Disable item removal without affecting remove mode.
+ */
+ public void setRemoveEnabled(boolean enabled) {
+ mRemoveEnabled = enabled;
+ }
+
+ /**
+ * @return True if remove is enabled, false otherwise.
+ */
+ public boolean isRemoveEnabled() {
+ return mRemoveEnabled;
+ }
+
+ /**
+ * Set the resource id for the View that represents the drag handle in a
+ * list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setDragHandleId(int id) {
+ mDragHandleId = id;
+ }
+
+ /**
+ * Sets flags to restrict certain motions of the floating View based on
+ * DragSortController settings (such as remove mode). Starts the drag on the
+ * DragSortListView.
+ *
+ * @param position The list item position (includes headers).
+ * @param deltaX Touch x-coord minus left edge of floating View.
+ * @param deltaY Touch y-coord minus top edge of floating View.
+ * @return True if drag started, false otherwise.
+ */
+ public boolean startDrag(int position, int deltaX, int deltaY) {
+
+ int mDragFlags = 0;
+ if (mSortEnabled) {
+ mDragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
+ }
+
+ if (mRemoveEnabled) {
+ if (mRemoveMode == FLING_RIGHT_REMOVE) {
+ mDragFlags |= DragSortListView.DRAG_POS_X;
+ } else if (mRemoveMode == FLING_LEFT_REMOVE) {
+ mDragFlags |= DragSortListView.DRAG_NEG_X;
+ }
+ }
+
+ mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), mDragFlags, deltaX,
+ deltaY);
+ return mDragging;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ mDetector.onTouchEvent(ev);
+ if (mRemoveEnabled && mDragging
+ && (mRemoveMode == FLING_RIGHT_REMOVE || mRemoveMode == FLING_LEFT_REMOVE)) {
+ mFlingRemoveDetector.onTouchEvent(ev);
+ }
+
+ final int mAction = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (mAction) {
+ case MotionEvent.ACTION_DOWN:
+ mCurrX = (int)ev.getX();
+ mCurrY = (int)ev.getY();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mRemoveEnabled) {
+ final int x = (int)ev.getX();
+ int thirdW = mDslv.getWidth() / 3;
+ int twoThirdW = mDslv.getWidth() - thirdW;
+ if ((mRemoveMode == SLIDE_RIGHT_REMOVE && x > twoThirdW)
+ || (mRemoveMode == SLIDE_LEFT_REMOVE && x < thirdW)) {
+ mDslv.stopDrag(true);
+ }
+ }
+ case MotionEvent.ACTION_CANCEL:
+ mDragging = false;
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Overrides to provide fading when slide removal is enabled.
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+
+ if (mRemoveEnabled) {
+ int x = touch.x;
+
+ if (mRemoveMode == SLIDE_RIGHT_REMOVE) {
+ int width = mDslv.getWidth();
+ int thirdWidth = width / 3;
+
+ float alpha;
+ if (x < thirdWidth) {
+ alpha = 1.0f;
+ } else if (x < width - thirdWidth) {
+ alpha = ((float)(width - thirdWidth - x)) / ((float)thirdWidth);
+ } else {
+ alpha = 0.0f;
+ }
+ mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+ } else if (mRemoveMode == SLIDE_LEFT_REMOVE) {
+ int width = mDslv.getWidth();
+ int thirdWidth = width / 3;
+
+ float alpha;
+ if (x < thirdWidth) {
+ alpha = 0.0f;
+ } else if (x < width - thirdWidth) {
+ alpha = ((float)(x - thirdWidth)) / ((float)thirdWidth);
+ } else {
+ alpha = 1.0f;
+ }
+ mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+ }
+ }
+ }
+
+ /**
+ * Get the position to start dragging based on the ACTION_DOWN MotionEvent.
+ * This function simply calls {@link #dragHandleHitPosition(MotionEvent)}.
+ * Override to change drag handle behavior; this function is called
+ * internally when an ACTION_DOWN event is detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ * @return The list position to drag if a drag-init gesture is detected;
+ * MISS if unsuccessful.
+ */
+ public int startDragPosition(MotionEvent ev) {
+ return dragHandleHitPosition(ev);
+ }
+
+ /**
+ * Checks for the touch of an item's drag handle (specified by
+ * {@link #setDragHandleId(int)}), and returns that item's position if a
+ * drag handle touch was detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ * @return The list position of the item whose drag handle was touched; MISS
+ * if unsuccessful.
+ */
+ public int dragHandleHitPosition(MotionEvent ev) {
+ final int x = (int)ev.getX();
+ final int y = (int)ev.getY();
+
+ int touchPos = mDslv.pointToPosition(x, y);
+
+ final int numHeaders = mDslv.getHeaderViewsCount();
+ final int numFooters = mDslv.getFooterViewsCount();
+ final int count = mDslv.getCount();
+
+ if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
+ && touchPos < (count - numFooters)) {
+ final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
+ final int rawX = (int)ev.getRawX();
+ final int rawY = (int)ev.getRawY();
+
+ View dragBox = item.findViewById(mDragHandleId);
+ if (dragBox != null) {
+ dragBox.getLocationOnScreen(mTempLoc);
+
+ if (rawX > mTempLoc[0] && rawY > mTempLoc[1]
+ && rawX < mTempLoc[0] + dragBox.getWidth()
+ && rawY < mTempLoc[1] + dragBox.getHeight()) {
+
+ mItemX = item.getLeft();
+ mItemY = item.getTop();
+
+ return touchPos;
+ }
+ }
+ }
+ return MISS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onDown(MotionEvent ev) {
+ mHitPos = startDragPosition(ev);
+
+ if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
+ startDrag(mHitPos, (int)ev.getX() - mItemX, (int)ev.getY() - mItemY);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (mHitPos != MISS && mDragInitMode == ON_DRAG && !mDragging) {
+ final int x1 = (int)e1.getX();
+ final int y1 = (int)e1.getY();
+ final int x2 = (int)e2.getX();
+ final int y2 = (int)e2.getY();
+
+ boolean start = false;
+ if (mRemoveEnabled && mSortEnabled) {
+ start = true;
+ } else if (mRemoveEnabled) {
+ start = Math.abs(x2 - x1) > mTouchSlop;
+ } else if (mSortEnabled) {
+ start = Math.abs(y2 - y1) > mTouchSlop;
+ }
+
+ if (start) {
+ startDrag(mHitPos, x2 - mItemX, y2 - mItemY);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
+ mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onSingleTapUp(MotionEvent ev) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onShowPress(MotionEvent ev) {
+ }
+
+ private final GestureDetector.OnGestureListener mFlingRemoveListener = new GestureDetector.SimpleOnGestureListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (mRemoveEnabled) {
+ switch (mRemoveMode) {
+ case FLING_RIGHT_REMOVE:
+ if (velocityX > mFlingSpeed) {
+ mDslv.stopDrag(true);
+ }
+ break;
+ case FLING_LEFT_REMOVE:
+ if (velocityX < -mFlingSpeed) {
+ mDslv.stopDrag(true);
+ }
+ break;
+ }
+ }
+ return false;
+ }
+ };
+
+}
diff --git a/src/com/cyngn/eleven/dragdrop/DragSortListView.java b/src/com/cyngn/eleven/dragdrop/DragSortListView.java
new file mode 100644
index 0000000..05ec499
--- /dev/null
+++ b/src/com/cyngn/eleven/dragdrop/DragSortListView.java
@@ -0,0 +1,2115 @@
+/*
+ * DragSortListView. A subclass of the Android ListView component that enables
+ * drag and drop re-ordering of list items. Copyright 2012 Carl Bauer Licensed
+ * under the Apache License, Version 2.0 (the "License"); you may not use this
+ * file except in compliance with the License. You may obtain a copy of the
+ * License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.dragdrop;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+
+import com.cyngn.eleven.R;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * ListView subclass that mediates drag and drop resorting of items.
+ *
+ * @author heycosmo
+ */
+public class DragSortListView extends ListView {
+
+ /**
+ * The View that floats above the ListView and represents the dragged item.
+ */
+ private View mFloatView;
+
+ /**
+ * A proposed float View location based on touch location and given deltaX
+ * and deltaY.
+ */
+ private final Point mFloatLoc = new Point();
+
+ /**
+ * The middle (in the y-direction) of the floating View.
+ */
+ private int mFloatViewMid;
+
+ /**
+ * Left edge of floating View.
+ */
+ private int mFloatViewLeft;
+
+ /**
+ * Top edge of floating View.
+ */
+ private int mFloatViewTop;
+
+ /**
+ * Watch the Adapter for data changes. Cancel a drag if coincident with a
+ * change.
+ */
+ private final DataSetObserver mObserver;
+
+ /**
+ * Transparency for the floating View (XML attribute).
+ */
+ private final float mFloatAlpha = 1.0f;
+
+ private float mCurrFloatAlpha = 1.0f;
+
+ /**
+ * While drag-sorting, the current position of the floating View. If
+ * dropped, the dragged item will land in this position.
+ */
+ private int mFloatPos;
+
+ /**
+ * The amount to scroll during the next layout pass. Used only for
+ * drag-scrolling, not standard ListView scrolling.
+ */
+ private int mScrollY = 0;
+
+ /**
+ * The first expanded ListView position that helps represent the drop slot
+ * tracking the floating View.
+ */
+ private int mFirstExpPos;
+
+ /**
+ * The second expanded ListView position that helps represent the drop slot
+ * tracking the floating View. This can equal mFirstExpPos if there is no
+ * slide shuffle occurring; otherwise it is equal to mFirstExpPos + 1.
+ */
+ private int mSecondExpPos;
+
+ /**
+ * Flag set if slide shuffling is enabled.
+ */
+ private boolean mAnimate = false;
+
+ /**
+ * The user dragged from this position.
+ */
+ private int mSrcPos;
+
+ /**
+ * Offset (in x) within the dragged item at which the user picked it up (or
+ * first touched down with the digitalis).
+ */
+ private int mDragDeltaX;
+
+ /**
+ * Offset (in y) within the dragged item at which the user picked it up (or
+ * first touched down with the digitalis).
+ */
+ private int mDragDeltaY;
+
+ /**
+ * A listener that receives callbacks whenever the floating View hovers over
+ * a new position.
+ */
+ private DragListener mDragListener;
+
+ /**
+ * A listener that receives a callback when the floating View is dropped.
+ */
+ private DropListener mDropListener;
+
+ /**
+ * A listener that receives a callback when the floating View (or more
+ * precisely the originally dragged item) is removed by one of the provided
+ * gestures.
+ */
+ private RemoveListener mRemoveListener;
+
+ /**
+ * Enable/Disable item dragging
+ */
+ private boolean mDragEnabled = true;
+
+ /**
+ * Drag state enum.
+ */
+ private final static int IDLE = 0;
+
+ private final static int STOPPED = 1;
+
+ private final static int DRAGGING = 2;
+
+ private int mDragState = IDLE;
+
+ /**
+ * Height in pixels to which the originally dragged item is collapsed during
+ * a drag-sort. Currently, this value must be greater than zero.
+ */
+ private int mItemHeightCollapsed = 1;
+
+ /**
+ * Height of the floating View. Stored for the purpose of providing the
+ * tracking drop slot.
+ */
+ private int mFloatViewHeight;
+
+ /**
+ * Convenience member. See above.
+ */
+ private int mFloatViewHeightHalf;
+
+ /**
+ * Save the given width spec for use in measuring children
+ */
+ private int mWidthMeasureSpec = 0;
+
+ /**
+ * Sample Views ultimately used for calculating the height of ListView items
+ * that are off-screen.
+ */
+ private View[] mSampleViewTypes = new View[1];
+
+ /**
+ * Drag-scroll encapsulator!
+ */
+ private final DragScroller mDragScroller;
+
+ /**
+ * Determines the start of the upward drag-scroll region at the top of the
+ * ListView. Specified by a fraction of the ListView height, thus screen
+ * resolution agnostic.
+ */
+ private float mDragUpScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * Determines the start of the downward drag-scroll region at the bottom of
+ * the ListView. Specified by a fraction of the ListView height, thus screen
+ * resolution agnostic.
+ */
+ private float mDragDownScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * The following are calculated from the above fracs.
+ */
+ private int mUpScrollStartY;
+
+ private int mDownScrollStartY;
+
+ private float mDownScrollStartYF;
+
+ private float mUpScrollStartYF;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragUpScrollHeight;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragDownScrollHeight;
+
+ /**
+ * Maximum drag-scroll speed in pixels per ms. Only used with default linear
+ * drag-scroll profile.
+ */
+ private float mMaxScrollSpeed = 0.3f;
+
+ /**
+ * Defines the scroll speed during a drag-scroll. User can provide their
+ * own; this default is a simple linear profile where scroll speed increases
+ * linearly as the floating View nears the top/bottom of the ListView.
+ */
+ private DragScrollProfile mScrollProfile = new DragScrollProfile() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ return mMaxScrollSpeed * w;
+ }
+ };
+
+ /**
+ * Current touch x.
+ */
+ private int mX;
+
+ /**
+ * Current touch y.
+ */
+ private int mY;
+
+ /**
+ * Last touch y.
+ */
+ private int mLastY;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive x direction.
+ */
+ public final static int DRAG_POS_X = 0x1;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative x direction.
+ */
+ public final static int DRAG_NEG_X = 0x2;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive y direction. This
+ * is subtle. What this actually means is that, if enabled, the floating
+ * View can be dragged below its starting position. Remove in favor of
+ * upper-bounding item position?
+ */
+ public final static int DRAG_POS_Y = 0x4;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative y direction. This
+ * is subtle. What this actually means is that the floating View can be
+ * dragged above its starting position. Remove in favor of lower-bounding
+ * item position?
+ */
+ public final static int DRAG_NEG_Y = 0x8;
+
+ /**
+ * Flags that determine limits on the motion of the floating View. See flags
+ * above.
+ */
+ private int mDragFlags = 0;
+
+ /**
+ * Last call to an on*TouchEvent was a call to onInterceptTouchEvent.
+ */
+ private boolean mLastCallWasIntercept = false;
+
+ /**
+ * A touch event is in progress.
+ */
+ private boolean mInTouchEvent = false;
+
+ /**
+ * Let the user customize the floating View.
+ */
+ private FloatViewManager mFloatViewManager = null;
+
+ /**
+ * Given to ListView to cancel its action when a drag-sort begins.
+ */
+ private final MotionEvent mCancelEvent;
+
+ /**
+ * Enum telling where to cancel the ListView action when a drag-sort begins
+ */
+ private static final int NO_CANCEL = 0;
+
+ private static final int ON_TOUCH_EVENT = 1;
+
+ private static final int ON_INTERCEPT_TOUCH_EVENT = 2;
+
+ /**
+ * Where to cancel the ListView action when a drag-sort begins
+ */
+ private int mCancelMethod = NO_CANCEL;
+
+ /**
+ * Determines when a slide shuffle animation starts. That is, defines how
+ * close to the edge of the drop slot the floating View must be to initiate
+ * the slide.
+ */
+ private float mSlideRegionFrac = 0.25f;
+
+ /**
+ * Number between 0 and 1 indicating the relative location of a sliding item
+ * (only used if drag-sort animations are turned on). Nearly 1 means the
+ * item is at the top of the slide region (nearly full blank item is
+ * directly below).
+ */
+ private float mSlideFrac = 0.0f;
+
+ /**
+ * Wraps the user-provided ListAdapter. This is used to wrap each item View
+ * given by the user inside another View (currenly a RelativeLayout) which
+ * expands and collapses to simulate the item shuffling.
+ */
+ private AdapterWrapper mAdapterWrapper;
+
+ /**
+ * Turn on custom debugger.
+ */
+ private final boolean mTrackDragSort = false;
+
+ /**
+ * Debugging class.
+ */
+ private DragSortTracker mDragSortTracker;
+
+ /**
+ * Needed for adjusting item heights from within layoutChildren
+ */
+ private boolean mBlockLayoutRequests = false;
+
+ private final DragSortController mController;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public DragSortListView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mItemHeightCollapsed = 1;
+
+ mCurrFloatAlpha = mFloatAlpha;
+
+ mSlideRegionFrac = 0.75f;
+
+ mAnimate = mSlideRegionFrac > 0.0f;
+
+ setDragScrollStart(mDragUpScrollStartFrac);
+
+ mController = new DragSortController(this, R.id.edit_track_list_item_handle,
+ DragSortController.ON_DOWN, DragSortController.FLING_RIGHT_REMOVE);
+ mController.setRemoveEnabled(true);
+ mController.setSortEnabled(true);
+ /* Transparent holo light blue */
+ mController
+ .setBackgroundColor(getResources().getColor(R.color.holo_blue_light_transparent));
+
+ mFloatViewManager = mController;
+ setOnTouchListener(mController);
+
+ mDragScroller = new DragScroller();
+ setOnScrollListener(mDragScroller);
+
+ mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f,
+ 0f, 0, 0);
+
+ mObserver = new DataSetObserver() {
+ private void cancel() {
+ if (mDragState == DRAGGING) {
+ stopDrag(false);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onChanged() {
+ cancel();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onInvalidated() {
+ cancel();
+ }
+ };
+ }
+
+ /**
+ * Usually called from a FloatViewManager. The float alpha will be reset to
+ * the xml-defined value every time a drag is stopped.
+ */
+ public void setFloatAlpha(final float alpha) {
+ mCurrFloatAlpha = alpha;
+ }
+
+ public float getFloatAlpha() {
+ return mCurrFloatAlpha;
+ }
+
+ /**
+ * Set maximum drag scroll speed in positions/second. Only applies if using
+ * default ScrollSpeedProfile.
+ *
+ * @param max Maximum scroll speed.
+ */
+ public void setMaxScrollSpeed(final float max) {
+ mMaxScrollSpeed = max;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAdapter(final ListAdapter adapter) {
+ mAdapterWrapper = new AdapterWrapper(adapter);
+ adapter.registerDataSetObserver(mObserver);
+ super.setAdapter(mAdapterWrapper);
+ }
+
+ /**
+ * As opposed to {@link ListView#getAdapter()}, which returns a heavily
+ * wrapped ListAdapter (DragSortListView wraps the input ListAdapter {\emph
+ * and} ListView wraps the wrapped one).
+ *
+ * @return The ListAdapter set as the argument of {@link setAdapter()}
+ */
+ public ListAdapter getInputAdapter() {
+ if (mAdapterWrapper == null) {
+ return null;
+ } else {
+ return mAdapterWrapper.getAdapter();
+ }
+ }
+
+ private class AdapterWrapper extends HeaderViewListAdapter {
+ private final ListAdapter mAdapter;
+
+ public AdapterWrapper(final ListAdapter adapter) {
+ super(null, null, adapter);
+ mAdapter = adapter;
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+
+ RelativeLayout v;
+ View child;
+ if (convertView != null) {
+
+ v = (RelativeLayout)convertView;
+ final View oldChild = v.getChildAt(0);
+ try {
+ child = mAdapter.getView(position, oldChild, v);
+ if (child != oldChild) {
+ v.removeViewAt(0);
+ v.addView(child);
+ }
+ } catch (final Exception nullz) {
+
+ }
+ } else {
+ final AbsListView.LayoutParams params = new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ v = new RelativeLayout(getContext());
+ v.setLayoutParams(params);
+ try {
+ child = mAdapter.getView(position, null, v);
+ v.addView(child);
+ } catch (final Exception todo) {
+
+ }
+ }
+ adjustItem(position + getHeaderViewsCount(), v, true);
+ return v;
+ }
+ }
+
+ private void drawDivider(final int expPosition, final Canvas canvas) {
+
+ final Drawable divider = getDivider();
+ final int dividerHeight = getDividerHeight();
+
+ if (divider != null && dividerHeight != 0) {
+ final ViewGroup expItem = (ViewGroup)getChildAt(expPosition - getFirstVisiblePosition());
+ if (expItem != null) {
+ final int l = getPaddingLeft();
+ final int r = getWidth() - getPaddingRight();
+ final int t;
+ final int b;
+
+ final int childHeight = expItem.getChildAt(0).getHeight();
+
+ if (expPosition > mSrcPos) {
+ t = expItem.getTop() + childHeight;
+ b = t + dividerHeight;
+ } else {
+ b = expItem.getBottom() - childHeight;
+ t = b - dividerHeight;
+ }
+
+ divider.setBounds(l, t, r, b);
+ divider.draw(canvas);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dispatchDraw(final Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (mFloatView != null) {
+ if (mFirstExpPos != mSrcPos) {
+ drawDivider(mFirstExpPos, canvas);
+ }
+ if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
+ drawDivider(mSecondExpPos, canvas);
+ }
+
+ final int w = mFloatView.getWidth();
+ final int h = mFloatView.getHeight();
+ final int alpha = (int)(255f * mCurrFloatAlpha);
+
+ canvas.save();
+ canvas.translate(mFloatViewLeft, mFloatViewTop);
+ canvas.clipRect(0, 0, w, h);
+
+ canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
+ mFloatView.draw(canvas);
+ canvas.restore();
+ canvas.restore();
+ }
+ }
+
+ private class ItemHeights {
+ int item;
+
+ int child;
+ }
+
+ private void measureItemAndGetHeights(final int position, final View item,
+ final ItemHeights heights) {
+ ViewGroup.LayoutParams lp = item.getLayoutParams();
+
+ final boolean isHeadFoot = position < getHeaderViewsCount()
+ || position >= getCount() - getFooterViewsCount();
+
+ int height = lp == null ? 0 : lp.height;
+ if (height > 0) {
+ heights.item = height;
+
+ // get height of child, measure if we have to
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ final View child = ((ViewGroup)item).getChildAt(0);
+ lp = child.getLayoutParams();
+ height = lp == null ? 0 : lp.height;
+ if (height > 0) {
+ heights.child = height;
+ } else {
+ final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ getListPaddingLeft() + getListPaddingRight(), lp.width);
+ child.measure(wspec, hspec);
+ heights.child = child.getMeasuredHeight();
+ }
+ }
+ } else {
+ final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp == null ? ViewGroup.LayoutParams.MATCH_PARENT
+ : lp.width);
+ item.measure(wspec, hspec);
+
+ heights.item = item.getMeasuredHeight();
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ heights.child = ((ViewGroup)item).getChildAt(0).getMeasuredHeight();
+ }
+ }
+ }
+
+ /**
+ * Get the height of the given wrapped item and its child.
+ *
+ * @param position Position from which item was obtained.
+ * @param item List item (usually obtained from
+ * {@link ListView#getChildAt()}).
+ * @param heights Object to fill with heights of item.
+ */
+ private void getItemHeights(final int position, final View item, final ItemHeights heights) {
+ final boolean isHeadFoot = position < getHeaderViewsCount()
+ || position >= getCount() - getFooterViewsCount();
+
+ heights.item = item.getHeight();
+
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ heights.child = ((ViewGroup)item).getChildAt(0).getHeight();
+ }
+ }
+
+ /**
+ * This function works for arbitrary positions (could be off-screen). If
+ * requested position is off-screen, this function calls
+ * <code>getView</code> to get height information.
+ *
+ * @param position ListView position.
+ * @param heights Object to fill with heights of item at
+ * <code>position</code>.
+ */
+ private void getItemHeights(final int position, final ItemHeights heights) {
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ if (position >= first && position <= last) {
+ getItemHeights(position, getChildAt(position - first), heights);
+ } else {
+ // Log.d("mobeta", "getView for height");
+
+ final ListAdapter adapter = getAdapter();
+ final int type = adapter.getItemViewType(position);
+
+ // There might be a better place for checking for the following
+ final int typeCount = adapter.getViewTypeCount();
+ if (typeCount != mSampleViewTypes.length) {
+ mSampleViewTypes = new View[typeCount];
+ }
+
+ View v;
+ if (type >= 0) {
+ if (mSampleViewTypes[type] == null) {
+ v = adapter.getView(position, null, this);
+ mSampleViewTypes[type] = v;
+ } else {
+ v = adapter.getView(position, mSampleViewTypes[type], this);
+ }
+ } else {
+ // type is HEADER_OR_FOOTER or IGNORE
+ v = adapter.getView(position, null, this);
+ }
+
+ measureItemAndGetHeights(position, v, heights);
+ }
+
+ }
+
+ private int getShuffleEdge(final int position, final int top) {
+ return getShuffleEdge(position, top, null);
+ }
+
+ /**
+ * Get the shuffle edge for item at position when top of item is at y-coord
+ * top
+ *
+ * @param position
+ * @param top
+ * @param height Height of item at position. If -1, this function calculates
+ * this height.
+ * @return Shuffle line between position-1 and position (for the given view
+ * of the list; that is, for when top of item at position has
+ * y-coord of given `top`). If floating View (treated as horizontal
+ * line) is dropped immediately above this line, it lands in
+ * position-1. If dropped immediately below this line, it lands in
+ * position.
+ */
+ private int getShuffleEdge(final int position, final int top, ItemHeights heights) {
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ // shuffle edges are defined between items that can be
+ // dragged; there are N-1 of them if there are N draggable
+ // items.
+
+ if (position <= numHeaders || position >= getCount() - numFooters) {
+ return top;
+ }
+
+ final int divHeight = getDividerHeight();
+
+ int edge;
+
+ final int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+
+ if (heights == null) {
+ heights = new ItemHeights();
+ getItemHeights(position, heights);
+ }
+
+ // first calculate top of item given that floating View is
+ // centered over src position
+ int otop = top;
+ if (mSecondExpPos <= mSrcPos) {
+ // items are expanded on and/or above the source position
+
+ if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ if (position == mSrcPos) {
+ otop = top + heights.item - mFloatViewHeight;
+ } else {
+ final int blankHeight = heights.item - heights.child;
+ otop = top + blankHeight - maxBlankHeight;
+ }
+ } else if (position > mSecondExpPos && position <= mSrcPos) {
+ otop = top - maxBlankHeight;
+ }
+
+ } else {
+ // items are expanded on and/or below the source position
+
+ if (position > mSrcPos && position <= mFirstExpPos) {
+ otop = top + maxBlankHeight;
+ } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ final int blankHeight = heights.item - heights.child;
+ otop = top + blankHeight;
+ }
+ }
+
+ // otop is set
+ if (position <= mSrcPos) {
+ final ItemHeights tmpHeights = new ItemHeights();
+ getItemHeights(position - 1, tmpHeights);
+ edge = otop + (mFloatViewHeight - divHeight - tmpHeights.child) / 2;
+ } else {
+ edge = otop + (heights.child - divHeight - mFloatViewHeight) / 2;
+ }
+
+ return edge;
+ }
+
+ private boolean updatePositions() {
+
+ final int first = getFirstVisiblePosition();
+ int startPos = mFirstExpPos;
+ View startView = getChildAt(startPos - first);
+
+ if (startView == null) {
+ startPos = first + getChildCount() / 2;
+ startView = getChildAt(startPos - first);
+ }
+ final int startTop = startView.getTop() + mScrollY;
+
+ final ItemHeights itemHeights = new ItemHeights();
+ getItemHeights(startPos, startView, itemHeights);
+
+ int edge = getShuffleEdge(startPos, startTop, itemHeights);
+ int lastEdge = edge;
+
+ final int divHeight = getDividerHeight();
+
+ // Log.d("mobeta", "float mid="+mFloatViewMid);
+
+ int itemPos = startPos;
+ int itemTop = startTop;
+ if (mFloatViewMid < edge) {
+ // scanning up for float position
+ // Log.d("mobeta", " edge="+edge);
+ while (itemPos >= 0) {
+ itemPos--;
+ getItemHeights(itemPos, itemHeights);
+
+ // if (itemPos <= 0)
+ if (itemPos == 0) {
+ edge = itemTop - divHeight - itemHeights.item;
+ // itemPos = 0;
+ break;
+ }
+
+ itemTop -= itemHeights.item + divHeight;
+ edge = getShuffleEdge(itemPos, itemTop, itemHeights);
+ // Log.d("mobeta", " edge="+edge);
+
+ if (mFloatViewMid >= edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ }
+ } else {
+ // scanning down for float position
+ // Log.d("mobeta", " edge="+edge);
+ final int count = getCount();
+ while (itemPos < count) {
+ if (itemPos == count - 1) {
+ edge = itemTop + divHeight + itemHeights.item;
+ break;
+ }
+
+ itemTop += divHeight + itemHeights.item;
+ getItemHeights(itemPos + 1, itemHeights);
+ edge = getShuffleEdge(itemPos + 1, itemTop, itemHeights);
+ // Log.d("mobeta", " edge="+edge);
+
+ // test for hit
+ if (mFloatViewMid < edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ itemPos++;
+ }
+ }
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ boolean updated = false;
+
+ final int oldFirstExpPos = mFirstExpPos;
+ final int oldSecondExpPos = mSecondExpPos;
+ final float oldSlideFrac = mSlideFrac;
+
+ if (mAnimate) {
+ final int edgeToEdge = Math.abs(edge - lastEdge);
+
+ int edgeTop, edgeBottom;
+ if (mFloatViewMid < edge) {
+ edgeBottom = edge;
+ edgeTop = lastEdge;
+ } else {
+ edgeTop = edge;
+ edgeBottom = lastEdge;
+ }
+ // Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);
+
+ final int slideRgnHeight = (int)(0.5f * mSlideRegionFrac * edgeToEdge);
+ final float slideRgnHeightF = slideRgnHeight;
+ final int slideEdgeTop = edgeTop + slideRgnHeight;
+ final int slideEdgeBottom = edgeBottom - slideRgnHeight;
+
+ // Three regions
+ if (mFloatViewMid < slideEdgeTop) {
+ mFirstExpPos = itemPos - 1;
+ mSecondExpPos = itemPos;
+ mSlideFrac = 0.5f * (slideEdgeTop - mFloatViewMid) / slideRgnHeightF;
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ } else if (mFloatViewMid < slideEdgeBottom) {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos + 1;
+ mSlideFrac = 0.5f * (1.0f + (edgeBottom - mFloatViewMid) / slideRgnHeightF);
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ }
+
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ // correct for headers and footers
+ if (mFirstExpPos < numHeaders) {
+ itemPos = numHeaders;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else if (mSecondExpPos >= getCount() - numFooters) {
+ itemPos = getCount() - numFooters - 1;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos
+ || mSlideFrac != oldSlideFrac) {
+ updated = true;
+ }
+
+ if (itemPos != mFloatPos) {
+ if (mDragListener != null) {
+ mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders);
+ }
+
+ mFloatPos = itemPos;
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mTrackDragSort) {
+ mDragSortTracker.appendState();
+ }
+ }
+
+ /**
+ * Stop a drag in progress. Pass <code>true</code> if you would like to
+ * remove the dragged item from the list.
+ *
+ * @param remove Remove the dragged item from the list. Calls a registered
+ * DropListener, if one exists.
+ * @return True if the stop was successful.
+ */
+ public boolean stopDrag(final boolean remove) {
+ if (mFloatView != null) {
+ mDragState = STOPPED;
+
+ // stop the drag
+ dropFloatView(remove);
+
+ return true;
+ } else {
+ // stop failed
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent ev) {
+
+ if (!mDragEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ boolean more = false;
+
+ final boolean lastCallWasIntercept = mLastCallWasIntercept;
+ mLastCallWasIntercept = false;
+
+ if (!lastCallWasIntercept) {
+ saveTouchCoords(ev);
+ }
+
+ if (mFloatView != null) {
+ onDragTouchEvent(ev);
+ more = true; // give us more!
+ } else {
+ // what if float view is null b/c we dropped in middle
+ // of drag touch event?
+
+ if (mDragState != STOPPED) {
+ if (super.onTouchEvent(ev)) {
+ more = true;
+ }
+ }
+
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (more) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ }
+ }
+ }
+
+ return more;
+
+ }
+
+ private void doActionUpOrCancel() {
+ mCancelMethod = NO_CANCEL;
+ mInTouchEvent = false;
+ mDragState = IDLE;
+ mCurrFloatAlpha = mFloatAlpha;
+ }
+
+ private void saveTouchCoords(final MotionEvent ev) {
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ if (action != MotionEvent.ACTION_DOWN) {
+ mLastY = mY;
+ }
+ mX = (int)ev.getX();
+ mY = (int)ev.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastY = mY;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent ev) {
+ if (!mDragEnabled) {
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ saveTouchCoords(ev);
+ mLastCallWasIntercept = true;
+
+ boolean intercept = false;
+
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mInTouchEvent = true;
+ }
+
+ // the following deals with calls to super.onInterceptTouchEvent
+ if (mFloatView != null) {
+ // super's touch event canceled in startDrag
+ intercept = true;
+ } else {
+ if (super.onInterceptTouchEvent(ev)) {
+ intercept = true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (intercept) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ } else {
+ mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
+ }
+ }
+ }
+
+ // check for startDragging
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mInTouchEvent = false;
+ }
+
+ return intercept;
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying a fraction of the
+ * ListView height.
+ *
+ * @param heightFraction Fraction of ListView height. Capped at 0.5f.
+ */
+ public void setDragScrollStart(final float heightFraction) {
+ setDragScrollStarts(heightFraction, heightFraction);
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying a fraction of the
+ * ListView height.
+ *
+ * @param upperFrac Fraction of ListView height for up-scroll bound. Capped
+ * at 0.5f.
+ * @param lowerFrac Fraction of ListView height for down-scroll bound.
+ * Capped at 0.5f.
+ */
+ public void setDragScrollStarts(final float upperFrac, final float lowerFrac) {
+ if (lowerFrac > 0.5f) {
+ mDragDownScrollStartFrac = 0.5f;
+ } else {
+ mDragDownScrollStartFrac = lowerFrac;
+ }
+
+ if (upperFrac > 0.5f) {
+ mDragUpScrollStartFrac = 0.5f;
+ } else {
+ mDragUpScrollStartFrac = upperFrac;
+ }
+
+ if (getHeight() != 0) {
+ updateScrollStarts();
+ }
+ }
+
+ private void continueDrag(final int x, final int y) {
+
+ // Log.d("mobeta", "move");
+ dragView(x, y);
+
+ // if (mTrackDragSort) {
+ // mDragSortTracker.appendState();
+ // }
+
+ requestLayout();
+
+ final int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
+ final int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
+
+ // get the current scroll direction
+ final int currentScrollDir = mDragScroller.getScrollDir();
+
+ if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
+ // dragged down, it is below the down scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from up scroll to down scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling down
+ mDragScroller.startScrolling(DragScroller.DOWN);
+ } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
+ // dragged up, it is above the up scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from down scroll to up scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling up
+ mDragScroller.startScrolling(DragScroller.UP);
+ } else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
+ && mDragScroller.isScrolling()) {
+ // not in the upper nor in the lower drag-scroll regions but it is
+ // still scrolling
+
+ mDragScroller.stopScrolling(true);
+ }
+ }
+
+ private void updateScrollStarts() {
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ final float heightF = listHeight;
+
+ mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
+ mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
+
+ mUpScrollStartY = (int)mUpScrollStartYF;
+ mDownScrollStartY = (int)mDownScrollStartYF;
+
+ mDragUpScrollHeight = mUpScrollStartYF - padTop;
+ mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateScrollStarts();
+ }
+
+ private void dropFloatView(final boolean removeSrcItem) {
+
+ mDragScroller.stopScrolling(true);
+
+ if (removeSrcItem) {
+ if (mRemoveListener != null) {
+ mRemoveListener.remove(mSrcPos - getHeaderViewsCount());
+ }
+ } else {
+ if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
+ final int numHeaders = getHeaderViewsCount();
+ mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
+ }
+
+ // adjustAllItems();
+
+ final int firstPos = getFirstVisiblePosition();
+ if (mSrcPos < firstPos) {
+ // collapsed src item is off screen;
+ // adjust the scroll after item heights have been fixed
+ final View v = getChildAt(0);
+ int top = 0;
+ if (v != null) {
+ top = v.getTop();
+ }
+ // Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
+ setSelectionFromTop(firstPos - 1, top - getPaddingTop());
+ }
+ }
+
+ mSrcPos = -1;
+ mFirstExpPos = -1;
+ mSecondExpPos = -1;
+ mFloatPos = -1;
+
+ removeFloatView();
+
+ if (mTrackDragSort) {
+ mDragSortTracker.stopTracking();
+ }
+ }
+
+ private void adjustAllItems() {
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ final int begin = Math.max(0, getHeaderViewsCount() - first);
+ final int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
+
+ for (int i = begin; i <= end; ++i) {
+ final View v = getChildAt(i);
+ if (v != null) {
+ adjustItem(first + i, v, false);
+ }
+ }
+ }
+
+ private void adjustItem(final int position, final View v, final boolean needsMeasure) {
+
+ final ViewGroup.LayoutParams lp = v.getLayoutParams();
+ final int oldHeight = lp.height;
+ int height = oldHeight;
+
+ getDividerHeight();
+
+ final boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
+ final int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+ final int slideHeight = (int)(mSlideFrac * maxNonSrcBlankHeight);
+
+ if (position == mSrcPos) {
+ if (mSrcPos == mFirstExpPos) {
+ if (isSliding) {
+ height = slideHeight + mItemHeightCollapsed;
+ } else {
+ height = mFloatViewHeight;
+ }
+ } else if (mSrcPos == mSecondExpPos) {
+ // if gets here, we know an item is sliding
+ height = mFloatViewHeight - slideHeight;
+ } else {
+ height = mItemHeightCollapsed;
+ }
+ } else if (position == mFirstExpPos || position == mSecondExpPos) {
+ // position is not src
+
+ final ItemHeights itemHeights = new ItemHeights();
+ if (needsMeasure) {
+ measureItemAndGetHeights(position, v, itemHeights);
+ } else {
+ getItemHeights(position, v, itemHeights);
+ }
+
+ if (position == mFirstExpPos) {
+ if (isSliding) {
+ height = itemHeights.child + slideHeight;
+ } else {
+ height = itemHeights.child + maxNonSrcBlankHeight;
+ }
+ } else { // position=mSecondExpPos
+ // we know an item is sliding (b/c 2ndPos != 1stPos)
+ height = itemHeights.child + maxNonSrcBlankHeight - slideHeight;
+ }
+ } else {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ }
+
+ if (height != oldHeight) {
+ lp.height = height;
+
+ v.setLayoutParams(lp);
+ }
+
+ // Adjust item gravity
+
+ if (position == mFirstExpPos || position == mSecondExpPos) {
+ if (position < mSrcPos) {
+ ((RelativeLayout)v).setGravity(Gravity.BOTTOM);
+ } else if (position > mSrcPos) {
+ ((RelativeLayout)v).setGravity(Gravity.TOP);
+ }
+ }
+
+ // Finally adjust item visibility
+
+ final int oldVis = v.getVisibility();
+ int vis = View.VISIBLE;
+
+ if (position == mSrcPos && mFloatView != null) {
+ vis = View.INVISIBLE;
+ }
+
+ if (vis != oldVis) {
+ v.setVisibility(vis);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests) {
+ super.requestLayout();
+ }
+ }
+
+ private void doDragScroll(final int oldFirstExpPos, final int oldSecondExpPos) {
+ if (mScrollY == 0) {
+ return;
+ }
+
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ int movePos;
+
+ if (mScrollY >= 0) {
+ mScrollY = Math.min(listHeight, mScrollY);
+ movePos = first;
+ } else {
+ mScrollY = Math.max(-listHeight, mScrollY);
+ movePos = last;
+ }
+
+ final View moveItem = getChildAt(movePos - first);
+ int top = moveItem.getTop() + mScrollY;
+
+ if (movePos == 0 && top > padTop) {
+ top = padTop;
+ }
+
+ final ItemHeights itemHeightsBefore = new ItemHeights();
+ getItemHeights(movePos, moveItem, itemHeightsBefore);
+ final int moveHeightBefore = itemHeightsBefore.item;
+ final int moveBlankBefore = moveHeightBefore - itemHeightsBefore.child;
+
+ final ItemHeights itemHeightsAfter = new ItemHeights();
+ measureItemAndGetHeights(movePos, moveItem, itemHeightsAfter);
+ final int moveHeightAfter = itemHeightsAfter.item;
+ final int moveBlankAfter = moveHeightAfter - itemHeightsAfter.child;
+
+ if (movePos <= oldFirstExpPos) {
+ if (movePos > mFirstExpPos) {
+ top += mFloatViewHeight - moveBlankAfter;
+ }
+ } else if (movePos == oldSecondExpPos) {
+ if (movePos <= mFirstExpPos) {
+ top += moveBlankBefore - mFloatViewHeight;
+ } else if (movePos == mSecondExpPos) {
+ top += moveHeightBefore - moveHeightAfter;
+ } else {
+ top += moveBlankBefore;
+ }
+ } else {
+ if (movePos <= mFirstExpPos) {
+ top -= mFloatViewHeight;
+ } else if (movePos == mSecondExpPos) {
+ top -= moveBlankAfter;
+ }
+ }
+
+ setSelectionFromTop(movePos, top - padTop);
+
+ mScrollY = 0;
+ }
+
+ private void measureFloatView() {
+ if (mFloatView != null) {
+ ViewGroup.LayoutParams lp = mFloatView.getLayoutParams();
+ if (lp == null) {
+ lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp.width);
+ int hspec;
+ if (lp.height > 0) {
+ hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ } else {
+ hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ mFloatView.measure(wspec, hspec);
+ mFloatViewHeight = mFloatView.getMeasuredHeight();
+ mFloatViewHeightHalf = mFloatViewHeight / 2;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested()) {
+ measureFloatView();
+ }
+ }
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void layoutChildren() {
+
+ if (mFloatView != null) {
+ mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
+
+ // Log.d("mobeta", "layout children");
+ final int oldFirstExpPos = mFirstExpPos;
+ final int oldSecondExpPos = mSecondExpPos;
+
+ mBlockLayoutRequests = true;
+
+ if (updatePositions()) {
+ adjustAllItems();
+ }
+
+ if (mScrollY != 0) {
+ doDragScroll(oldFirstExpPos, oldSecondExpPos);
+ }
+
+ mBlockLayoutRequests = false;
+ }
+
+ super.layoutChildren();
+ }
+
+ protected boolean onDragTouchEvent(final MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ stopDrag(false);
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ continueDrag((int)ev.getX(), (int)ev.getY());
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> using the registered
+ * FloatViewManager. Calls through to
+ * {@link #startDrag(int,View,int,int,int)} after obtaining the floating
+ * View from the FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param dragFlags Flags that restrict some movements of the floating View.
+ * For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
+ * directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the left edge of
+ * the floating View (i.e. touch-x minus float View left).
+ * @param deltaY Offset in y of the touch coordinate from the top edge of
+ * the floating View (i.e. touch-y minus float View top).
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in a
+ * touch event, there is no registered FloatViewManager, or the
+ * FloatViewManager returns a null View.
+ */
+ public boolean startDrag(final int position, final int dragFlags, final int deltaX,
+ final int deltaY) {
+ if (!mInTouchEvent || mFloatViewManager == null) {
+ return false;
+ }
+
+ final View v = mFloatViewManager.onCreateFloatView(position);
+
+ if (v == null) {
+ return false;
+ } else {
+ return startDrag(position, v, dragFlags, deltaX, deltaY);
+ }
+
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> without using a
+ * FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param floatView Floating View.
+ * @param dragFlags Flags that restrict some movements of the floating View.
+ * For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
+ * directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the left edge of
+ * the floating View (i.e. touch-x minus float View left).
+ * @param deltaY Offset in y of the touch coordinate from the top edge of
+ * the floating View (i.e. touch-y minus float View top).
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in a
+ * touch event, <code>floatView</code> is null, or there is a drag
+ * in progress.
+ */
+ public boolean startDrag(final int position, final View floatView, final int dragFlags,
+ final int deltaX, final int deltaY) {
+ if (!mInTouchEvent || mFloatView != null || floatView == null) {
+ return false;
+ }
+
+ if (getParent() != null) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+
+ final int pos = position + getHeaderViewsCount();
+ mFirstExpPos = pos;
+ mSecondExpPos = pos;
+ mSrcPos = pos;
+ mFloatPos = pos;
+
+ // mDragState = dragType;
+ mDragState = DRAGGING;
+ mDragFlags = 0;
+ mDragFlags |= dragFlags;
+
+ mFloatView = floatView;
+ measureFloatView(); // sets mFloatViewHeight
+
+ mDragDeltaX = deltaX;
+ mDragDeltaY = deltaY;
+ updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
+
+ // set src item invisible
+ final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
+ if (srcItem != null) {
+ srcItem.setVisibility(View.INVISIBLE);
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.startTracking();
+ }
+
+ // once float view is created, events are no longer passed
+ // to ListView
+ switch (mCancelMethod) {
+ case ON_TOUCH_EVENT:
+ super.onTouchEvent(mCancelEvent);
+ break;
+ case ON_INTERCEPT_TOUCH_EVENT:
+ super.onInterceptTouchEvent(mCancelEvent);
+ break;
+ }
+
+ requestLayout();
+
+ return true;
+ }
+
+ /**
+ * Sets float View location based on suggested values and constraints set in
+ * mDragFlags.
+ */
+ private void updateFloatView(final int floatX, final int floatY) {
+
+ // restrict x motion
+ final int padLeft = getPaddingLeft();
+ if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
+ mFloatViewLeft = padLeft;
+ } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
+ mFloatViewLeft = padLeft;
+ } else {
+ mFloatViewLeft = floatX;
+ }
+
+ // keep floating view from going past bottom of last header view
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+ final int firstPos = getFirstVisiblePosition();
+ final int lastPos = getLastVisiblePosition();
+
+ // Log.d("mobeta",
+ // "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
+ int topLimit = getPaddingTop();
+ if (firstPos < numHeaders) {
+ topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
+ }
+ if ((mDragFlags & DRAG_NEG_Y) == 0) {
+ if (firstPos <= mSrcPos) {
+ topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
+ }
+ }
+ // bottom limit is top of first footer View or
+ // bottom of last item in list
+ int bottomLimit = getHeight() - getPaddingBottom();
+ if (lastPos >= getCount() - numFooters - 1) {
+ bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
+ }
+ if ((mDragFlags & DRAG_POS_Y) == 0) {
+ if (lastPos >= mSrcPos) {
+ bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
+ }
+ }
+
+ // Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
+ // Log.d("mobeta", "limit=" + limit);
+ // Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
+
+ if (floatY < topLimit) {
+ mFloatViewTop = topLimit;
+ } else if (floatY + mFloatViewHeight > bottomLimit) {
+ mFloatViewTop = bottomLimit - mFloatViewHeight;
+ } else {
+ mFloatViewTop = floatY;
+ }
+
+ // get y-midpoint of floating view (constrained to ListView bounds)
+ mFloatViewMid = mFloatViewTop + mFloatViewHeightHalf;
+ }
+
+ private void dragView(final int x, final int y) {
+ // Log.d("mobeta", "float view pure x=" + x + " y=" + y);
+
+ // proposed position
+ mFloatLoc.x = x - mDragDeltaX;
+ mFloatLoc.y = y - mDragDeltaY;
+
+ final Point touch = new Point(x, y);
+
+ // let manager adjust proposed position first
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, touch);
+ }
+
+ // then we override if manager gives an unsatisfactory
+ // position (e.g. over a header/footer view). Also,
+ // dragFlags override manager adjustments.
+ updateFloatView(mFloatLoc.x, mFloatLoc.y);
+ }
+
+ private void removeFloatView() {
+ if (mFloatView != null) {
+ mFloatView.setVisibility(GONE);
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDestroyFloatView(mFloatView);
+ }
+ mFloatView = null;
+ }
+ }
+
+ /**
+ * Interface for customization of the floating View appearance and dragging
+ * behavior. Implement your own and pass it to {@link #setFloatViewManager}.
+ * If your own is not passed, the default {@link SimpleFloatViewManager}
+ * implementation is used.
+ */
+ public interface FloatViewManager {
+ /**
+ * Return the floating View for item at <code>position</code>.
+ * DragSortListView will measure and layout this View for you, so feel
+ * free to just inflate it. You can help DSLV by setting some
+ * {@link ViewGroup.LayoutParams} on this View; otherwise it will set
+ * some for you (with a width of FILL_PARENT and a height of
+ * WRAP_CONTENT).
+ *
+ * @param position Position of item to drag (NOTE: <code>position</code>
+ * excludes header Views; thus, if you want to call
+ * {@link ListView#getChildAt(int)}, you will need to add
+ * {@link ListView#getHeaderViewsCount()} to the index).
+ * @return The View you wish to display as the floating View.
+ */
+ public View onCreateFloatView(int position);
+
+ /**
+ * Called whenever the floating View is dragged. Float View properties
+ * can be changed here. Also, the upcoming location of the float View
+ * can be altered by setting <code>location.x</code> and
+ * <code>location.y</code>.
+ *
+ * @param floatView The floating View.
+ * @param location The location (top-left; relative to DSLV top-left) at
+ * which the float View would like to appear, given the
+ * current touch location and the offset provided in
+ * {@link DragSortListView#startDrag}.
+ * @param touch The current touch location (relative to DSLV top-left).
+ */
+ public void onDragFloatView(View floatView, Point location, Point touch);
+
+ /**
+ * Called when the float View is dropped; lets you perform any necessary
+ * cleanup. The internal DSLV floating View reference is set to null
+ * immediately after this is called.
+ *
+ * @param floatView The floating View passed to
+ * {@link #onCreateFloatView(int)}.
+ */
+ public void onDestroyFloatView(View floatView);
+ }
+
+ public void setFloatViewManager(final FloatViewManager manager) {
+ mFloatViewManager = manager;
+ }
+
+ public void setDragListener(final DragListener l) {
+ mDragListener = l;
+ }
+
+ /**
+ * Allows for easy toggling between a DragSortListView and a regular old
+ * ListView. If enabled, items are draggable, where the drag init mode
+ * determines how items are lifted (see {@link setDragInitMode(int)}). If
+ * disabled, items cannot be dragged.
+ *
+ * @param enabled Set <code>true</code> to enable list item dragging
+ */
+ public void setDragEnabled(final boolean enabled) {
+ mDragEnabled = enabled;
+ }
+
+ public boolean isDragEnabled() {
+ return mDragEnabled;
+ }
+
+ /**
+ * This better reorder your ListAdapter! DragSortListView does not do this
+ * for you; doesn't make sense to. Make sure
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
+ * in your implementation.
+ *
+ * @param l
+ */
+ public void setDropListener(final DropListener l) {
+ mDropListener = l;
+ }
+
+ /**
+ * Probably a no-brainer, but make sure that your remove listener calls
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it. When an
+ * item removal occurs, DragSortListView relies on a redraw of all the items
+ * to recover invisible views and such. Strictly speaking, if you remove
+ * something, your dataset has changed...
+ *
+ * @param l
+ */
+ public void setRemoveListener(final RemoveListener l) {
+ if (mController != null && l == null) {
+ mController.setRemoveEnabled(false);
+ }
+ mRemoveListener = l;
+ }
+
+ public interface DragListener {
+ public void drag(int from, int to);
+ }
+
+ /**
+ * Your implementation of this has to reorder your ListAdapter! Make sure to
+ * call {@link BaseAdapter#notifyDataSetChanged()} or something like it in
+ * your implementation.
+ *
+ * @author heycosmo
+ */
+ public interface DropListener {
+ public void drop(int from, int to);
+ }
+
+ /**
+ * Make sure to call {@link BaseAdapter#notifyDataSetChanged()} or something
+ * like it in your implementation.
+ *
+ * @author heycosmo
+ */
+ public interface RemoveListener {
+ public void remove(int which);
+ }
+
+ public interface DragSortListener extends DropListener, DragListener, RemoveListener {
+ }
+
+ public void setDragSortListener(final DragSortListener l) {
+ setDropListener(l);
+ setDragListener(l);
+ setRemoveListener(l);
+ }
+
+ /**
+ * Completely custom scroll speed profile. Default increases linearly with
+ * position and is constant in time. Create your own by implementing
+ * {@link DragSortListView.DragScrollProfile}.
+ *
+ * @param ssp
+ */
+ public void setDragScrollProfile(final DragScrollProfile ssp) {
+ if (ssp != null) {
+ mScrollProfile = ssp;
+ }
+ }
+
+ /**
+ * Interface for controlling scroll speed as a function of touch position
+ * and time. Use
+ * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to set
+ * custom profile.
+ *
+ * @author heycosmo
+ */
+ public interface DragScrollProfile {
+ /**
+ * Return a scroll speed in pixels/millisecond. Always return a positive
+ * number.
+ *
+ * @param w Normalized position in scroll region (i.e. w \in [0,1]).
+ * Small w typically means slow scrolling.
+ * @param t Time (in milliseconds) since start of scroll (handy if you
+ * want scroll acceleration).
+ * @return Scroll speed at position w and time t in pixels/ms.
+ */
+ float getSpeed(float w, long t);
+ }
+
+ private class DragScroller implements Runnable, AbsListView.OnScrollListener {
+
+ private boolean mAbort;
+
+ private long mPrevTime;
+
+ private int dy;
+
+ private float dt;
+
+ private long tStart;
+
+ private int scrollDir;
+
+ public final static int STOP = -1;
+
+ public final static int UP = 0;
+
+ public final static int DOWN = 1;
+
+ private float mScrollSpeed; // pixels per ms
+
+ private boolean mScrolling = false;
+
+ public boolean isScrolling() {
+ return mScrolling;
+ }
+
+ public int getScrollDir() {
+ return mScrolling ? scrollDir : STOP;
+ }
+
+ public DragScroller() {
+ }
+
+ public void startScrolling(final int dir) {
+ if (!mScrolling) {
+ // Debug.startMethodTracing("dslv-scroll");
+ mAbort = false;
+ mScrolling = true;
+ tStart = SystemClock.uptimeMillis();
+ mPrevTime = tStart;
+ scrollDir = dir;
+ post(this);
+ }
+ }
+
+ public void stopScrolling(final boolean now) {
+ if (now) {
+ removeCallbacks(this);
+ mScrolling = false;
+ } else {
+ mAbort = true;
+ }
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ if (mAbort) {
+ mScrolling = false;
+ return;
+ }
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+ final int count = getCount();
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+
+ final int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
+ final int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
+
+ if (scrollDir == UP) {
+ final View v = getChildAt(0);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (first == 0 && v.getTop() == padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY)
+ / mDragUpScrollHeight, mPrevTime);
+ } else {
+ final View v = getChildAt(last - first);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF)
+ / mDragDownScrollHeight, mPrevTime);
+ }
+
+ dt = SystemClock.uptimeMillis() - mPrevTime;
+ // dy is change in View position of a list item; i.e. positive dy
+ // means user is scrolling up (list item moves down the screen,
+ // remember
+ // y=0 is at top of View).
+ dy = Math.round(mScrollSpeed * dt);
+ mScrollY += dy;
+
+ requestLayout();
+
+ mPrevTime += dt;
+
+ post(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ if (mScrolling && visibleItemCount != 0) {
+ dragView(mX, mY);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ }
+
+ }
+
+ private class DragSortTracker {
+ StringBuilder mBuilder = new StringBuilder();
+
+ File mFile;
+
+ private int mNumInBuffer = 0;
+
+ private int mNumFlushes = 0;
+
+ private boolean mTracking = false;
+
+ public void startTracking() {
+ mBuilder.append("<DSLVStates>\n");
+ mNumFlushes = 0;
+ mTracking = true;
+ }
+
+ public void appendState() {
+ if (!mTracking) {
+ return;
+ }
+
+ mBuilder.append("<DSLVState>\n");
+ final int children = getChildCount();
+ final int first = getFirstVisiblePosition();
+ final ItemHeights itemHeights = new ItemHeights();
+ mBuilder.append(" <Positions>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(first + i).append(",");
+ }
+ mBuilder.append("</Positions>\n");
+
+ mBuilder.append(" <Tops>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getTop()).append(",");
+ }
+ mBuilder.append("</Tops>\n");
+ mBuilder.append(" <Bottoms>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getBottom()).append(",");
+ }
+ mBuilder.append("</Bottoms>\n");
+
+ mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
+ getItemHeights(mFirstExpPos, itemHeights);
+ mBuilder.append(" <FirstExpBlankHeight>")
+ .append(itemHeights.item - itemHeights.child)
+ .append("</FirstExpBlankHeight>\n");
+ mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
+ getItemHeights(mSecondExpPos, itemHeights);
+ mBuilder.append(" <SecondExpBlankHeight>")
+ .append(itemHeights.item - itemHeights.child)
+ .append("</SecondExpBlankHeight>\n");
+ mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
+ mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight())
+ .append("</SrcHeight>\n");
+ mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
+ mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n");
+ mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
+ mBuilder.append(" <ShuffleEdges>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
+ }
+ mBuilder.append("</ShuffleEdges>\n");
+
+ mBuilder.append("</DSLVState>\n");
+ mNumInBuffer++;
+
+ if (mNumInBuffer > 1000) {
+ flush();
+ mNumInBuffer = 0;
+ }
+ }
+
+ public void flush() {
+ if (!mTracking) {
+ return;
+ }
+
+ // save to file on sdcard
+ try {
+ boolean append = true;
+ if (mNumFlushes == 0) {
+ append = false;
+ }
+ final FileWriter writer = new FileWriter(mFile, append);
+
+ writer.write(mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+
+ writer.flush();
+ writer.close();
+
+ mNumFlushes++;
+ } catch (final IOException e) {
+ // do nothing
+ }
+ }
+
+ public void stopTracking() {
+ if (mTracking) {
+ mBuilder.append("</DSLVStates>\n");
+ flush();
+ mTracking = false;
+ }
+ }
+
+ }
+
+}
diff --git a/src/com/cyngn/eleven/dragdrop/SimpleFloatViewManager.java b/src/com/cyngn/eleven/dragdrop/SimpleFloatViewManager.java
new file mode 100644
index 0000000..1cf0d49
--- /dev/null
+++ b/src/com/cyngn/eleven/dragdrop/SimpleFloatViewManager.java
@@ -0,0 +1,77 @@
+
+package com.cyngn.eleven.dragdrop;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ListView;
+
+/**
+ * Simple implementation of the FloatViewManager class. Uses list items as they
+ * appear in the ListView to create the floating View.
+ */
+public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
+
+ private final ListView mListView;
+
+ private Bitmap mFloatBitmap;
+
+ private int mFloatBGColor = Color.BLACK;
+
+ public SimpleFloatViewManager(ListView lv) {
+ mListView = lv;
+ }
+
+ public void setBackgroundColor(int color) {
+ mFloatBGColor = color;
+ }
+
+ /**
+ * This simple implementation creates a Bitmap copy of the list item
+ * currently shown at ListView <code>position</code>.
+ */
+ @Override
+ public View onCreateFloatView(int position) {
+ View v = mListView.getChildAt(position + mListView.getHeaderViewsCount()
+ - mListView.getFirstVisiblePosition());
+
+ if (v == null) {
+ return null;
+ }
+
+ v.setPressed(false);
+
+ v.setDrawingCacheEnabled(true);
+ mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
+ v.setDrawingCacheEnabled(false);
+
+ ImageView iv = new ImageView(mListView.getContext());
+ iv.setBackgroundColor(mFloatBGColor);
+ iv.setPadding(0, 0, 0, 0);
+ iv.setImageBitmap(mFloatBitmap);
+
+ return iv;
+ }
+
+ /**
+ * Removes the Bitmap from the ImageView created in onCreateFloatView() and
+ * tells the system to recycle it.
+ */
+ @Override
+ public void onDestroyFloatView(View floatView) {
+ ((ImageView)floatView).setImageDrawable(null);
+
+ mFloatBitmap.recycle();
+ mFloatBitmap = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+ /* Nothing to do */
+ }
+}
diff --git a/src/com/cyngn/eleven/format/Capitalize.java b/src/com/cyngn/eleven/format/Capitalize.java
new file mode 100644
index 0000000..3d3a381
--- /dev/null
+++ b/src/com/cyngn/eleven/format/Capitalize.java
@@ -0,0 +1,60 @@
+
+package com.cyngn.eleven.format;
+
+import android.text.TextUtils;
+
+public class Capitalize {
+
+ /* This class is never initiated */
+ public Capitalize() {
+ }
+
+ public static final String capitalize(String str) {
+ return capitalize(str, null);
+ }
+
+ /**
+ * Capitalizes the first character in a string
+ *
+ * @param str The string to capitalize
+ * @param delimiters The delimiters
+ * @return A captitalized string
+ */
+ public static final String capitalize(String str, char... delimiters) {
+ final int delimLen = delimiters == null ? -1 : delimiters.length;
+ if (TextUtils.isEmpty(str) || delimLen == 0) {
+ return str;
+ }
+ final char[] buffer = str.toCharArray();
+ boolean capitalizeNext = true;
+ for (int i = 0; i < buffer.length; i++) {
+ char ch = buffer[i];
+ if (isDelimiter(ch, delimiters)) {
+ capitalizeNext = true;
+ } else if (capitalizeNext) {
+ buffer[i] = Character.toTitleCase(ch);
+ capitalizeNext = false;
+ }
+ }
+ return new String(buffer);
+ }
+
+ /**
+ * Is the character a delimiter.
+ *
+ * @param ch the character to check
+ * @param delimiters the delimiters
+ * @return true if it is a delimiter
+ */
+ private static final boolean isDelimiter(char ch, char[] delimiters) {
+ if (delimiters == null) {
+ return Character.isWhitespace(ch);
+ }
+ for (char delimiter : delimiters) {
+ if (ch == delimiter) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/cyngn/eleven/format/PrefixHighlighter.java b/src/com/cyngn/eleven/format/PrefixHighlighter.java
new file mode 100644
index 0000000..b4d82ec
--- /dev/null
+++ b/src/com/cyngn/eleven/format/PrefixHighlighter.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.format;
+
+import android.content.Context;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.widget.TextView;
+
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+/**
+ * Highlights the text in a text field.
+ */
+public class PrefixHighlighter {
+
+ /* Color used when highlighting the prefixes */
+ private final int mPrefixHighlightColor;
+
+ private ForegroundColorSpan mPrefixColorSpan;
+
+ /**
+ * @param prefixHighlightColor The color used to highlight the prefixes.
+ */
+ public PrefixHighlighter(final Context context) {
+ mPrefixHighlightColor = PreferenceUtils.getInstance(context).getDefaultThemeColor(context);
+ }
+
+ /**
+ * Sets the text on the given {@link TextView}, highlighting the word that
+ * matches the given prefix.
+ *
+ * @param view The {@link TextView} on which to set the text
+ * @param text The string to use as the text
+ * @param prefix The prefix to look for
+ */
+ public void setText(final TextView view, final String text, final char[] prefix) {
+ if (view == null || TextUtils.isEmpty(text) || prefix == null || prefix.length == 0) {
+ return;
+ }
+ view.setText(apply(text, prefix));
+ }
+
+ /**
+ * Returns a {@link CharSequence} which highlights the given prefix if found
+ * in the given text.
+ *
+ * @param text the text to which to apply the highlight
+ * @param prefix the prefix to look for
+ */
+ public CharSequence apply(final CharSequence text, final char[] prefix) {
+ final int mIndex = indexOfWordPrefix(text, prefix);
+ if (mIndex != -1) {
+ if (mPrefixColorSpan == null) {
+ mPrefixColorSpan = new ForegroundColorSpan(mPrefixHighlightColor);
+ }
+ final SpannableString mResult = new SpannableString(text);
+ mResult.setSpan(mPrefixColorSpan, mIndex, mIndex + prefix.length, 0);
+ return mResult;
+ } else {
+ return text;
+ }
+ }
+
+ /**
+ * Finds the index of the first word that starts with the given prefix. If
+ * not found, returns -1.
+ *
+ * @param text the text in which to search for the prefix
+ * @param prefix the text to find, in upper case letters
+ */
+ private int indexOfWordPrefix(final CharSequence text, final char[] prefix) {
+ if (TextUtils.isEmpty(text) || prefix == null) {
+ return -1;
+ }
+
+ final int mTextLength = text.length();
+ final int mPrefixLength = prefix.length;
+
+ if (mPrefixLength == 0 || mTextLength < mPrefixLength) {
+ return -1;
+ }
+
+ int i = 0;
+ while (i < mTextLength) {
+ /* Skip non-word characters */
+ while (i < mTextLength && !Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+
+ if (i + mPrefixLength > mTextLength) {
+ return -1;
+ }
+
+ /* Compare the prefixes */
+ int j;
+ for (j = 0; j < mPrefixLength; j++) {
+ if (Character.toUpperCase(text.charAt(i + j)) != prefix[j]) {
+ break;
+ }
+ }
+ if (j == mPrefixLength) {
+ return i;
+ }
+
+ /* Skip this word */
+ while (i < mTextLength && Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+ }
+ return -1;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/lastfm/Album.java b/src/com/cyngn/eleven/lastfm/Album.java
new file mode 100644
index 0000000..cd657de
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/Album.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import android.content.Context;
+
+import com.cyngn.eleven.Config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Wrapper class for Album related API calls and Album Bean.
+ *
+ * @author Janni Kovacs
+ */
+public class Album extends MusicEntry {
+
+ protected final static ItemFactory<Album> FACTORY = new AlbumFactory();
+
+ private String artist;
+
+ /**
+ * @param name
+ * @param url
+ * @param artist
+ */
+ private Album(final String name, final String url, final String artist) {
+ super(name, url);
+ this.artist = artist;
+ }
+
+ /**
+ * Get the metadata for an album on Last.fm using the album name or a
+ * musicbrainz id. See playlist.fetch on how to get the album playlist.
+ *
+ * @param artist Artist's name
+ * @param albumOrMbid Album name or MBID
+ * @return Album metadata
+ */
+ public final static Album getInfo(final Context context, final String artist,
+ final String albumOrMbid) {
+ return getInfo(context, artist, albumOrMbid, null, Config.LASTFM_API_KEY);
+ }
+
+ /**
+ * Get the metadata for an album on Last.fm using the album name or a
+ * musicbrainz id. See playlist.fetch on how to get the album playlist.
+ *
+ * @param artist Artist's name
+ * @param albumOrMbid Album name or MBID
+ * @param username The username for the context of the request. If supplied,
+ * the user's playcount for this album is included in the
+ * response.
+ * @param apiKey The API key
+ * @return Album metadata
+ */
+ public final static Album getInfo(final Context context, final String artist,
+ final String albumOrMbid, final String username, final String apiKey) {
+ final Map<String, String> params = new HashMap<String, String>();
+ params.put("artist", artist);
+ params.put("album", albumOrMbid);
+ MapUtilities.nullSafePut(params, "username", username);
+ final Result result = Caller.getInstance(context).call("album.getInfo", apiKey, params);
+ return ResponseBuilder.buildItem(result, Album.class);
+ }
+
+ private final static class AlbumFactory implements ItemFactory<Album> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Album createItemFromElement(final DomElement element) {
+ if (element == null) {
+ return null;
+ }
+ final Album album = new Album(null, null, null);
+ MusicEntry.loadStandardInfo(album, element);
+ if (element.hasChild("artist")) {
+ album.artist = element.getChild("artist").getChildText("name");
+ if (album.artist == null) {
+ album.artist = element.getChildText("artist");
+ }
+ }
+ return album;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/Artist.java b/src/com/cyngn/eleven/lastfm/Artist.java
new file mode 100644
index 0000000..0ffaf8c
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/Artist.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import android.content.Context;
+
+import com.cyngn.eleven.Config;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Bean that contains artist information.<br/>
+ * This class contains static methods that executes API methods relating to
+ * artists.<br/>
+ * Method names are equivalent to the last.fm API method names.
+ *
+ * @author Janni Kovacs
+ */
+public class Artist extends MusicEntry {
+
+ protected final static ItemFactory<Artist> FACTORY = new ArtistFactory();
+
+ protected Artist(final String name, final String url) {
+ super(name, url);
+ }
+
+ /**
+ * Retrieves detailed artist info for the given artist or mbid entry.
+ *
+ * @param artistOrMbid Name of the artist or an mbid
+ * @return detailed artist info
+ */
+ public final static Artist getInfo(final Context context, final String artistOrMbid) {
+ return getInfo(context, artistOrMbid, Locale.getDefault(), Config.LASTFM_API_KEY);
+ }
+
+ /**
+ * Retrieves detailed artist info for the given artist or mbid entry.
+ *
+ * @param artistOrMbid Name of the artist or an mbid
+ * @param locale The language to fetch info in, or <code>null</code>
+ * @param apiKey The API key
+ * @return detailed artist info
+ */
+ public final static Artist getInfo(final Context context, final String artistOrMbid,
+ final Locale locale, final String apiKey) {
+ final Map<String, String> mParams = new WeakHashMap<String, String>();
+ mParams.put("artist", artistOrMbid);
+ if (locale != null && locale.getLanguage().length() != 0) {
+ mParams.put("lang", locale.getLanguage());
+ }
+ final Result mResult = Caller.getInstance(context).call("artist.getInfo", apiKey, mParams);
+ return ResponseBuilder.buildItem(mResult, Artist.class);
+ }
+
+ /**
+ * Use the last.fm corrections data to check whether the supplied artist has
+ * a correction to a canonical artist. This method returns a new
+ * {@link Artist} object containing the corrected data, or <code>null</code>
+ * if the supplied Artist was not found.
+ *
+ * @param artist The artist name to correct
+ * @return a new {@link Artist}, or <code>null</code>
+ */
+ public final static Artist getCorrection(final Context context, final String artist) {
+ Result result = null;
+ try {
+ result = Caller.getInstance(context).call("artist.getCorrection",
+ Config.LASTFM_API_KEY, "artist", artist);
+ if (!result.isSuccessful()) {
+ return null;
+ }
+ final DomElement correctionElement = result.getContentElement().getChild("correction");
+ if (correctionElement == null) {
+ return new Artist(artist, null);
+ }
+ final DomElement artistElem = correctionElement.getChild("artist");
+ return FACTORY.createItemFromElement(artistElem);
+ } catch (final Exception ignored) {
+ return null;
+ }
+ }
+
+ private final static class ArtistFactory implements ItemFactory<Artist> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Artist createItemFromElement(final DomElement element) {
+ if (element == null) {
+ return null;
+ }
+ final Artist artist = new Artist(null, null);
+ MusicEntry.loadStandardInfo(artist, element);
+ return artist;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/Caller.java b/src/com/cyngn/eleven/lastfm/Caller.java
new file mode 100644
index 0000000..f5e6c45
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/Caller.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import static com.cyngn.eleven.lastfm.StringUtilities.encode;
+import static com.cyngn.eleven.lastfm.StringUtilities.map;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.cyngn.eleven.lastfm.Result.Status;
+
+import org.apache.http.HttpStatus;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * The <code>Caller</code> class handles the low-level communication between the
+ * client and last.fm.<br/>
+ * Direct usage of this class should be unnecessary since all method calls are
+ * available via the methods in the <code>Artist</code>, <code>Album</code>,
+ * <code>User</code>, etc. classes. If specialized calls which are not covered
+ * by the Java API are necessary this class may be used directly.<br/>
+ * Supports the setting of a custom {@link Proxy} and a custom
+ * <code>User-Agent</code> HTTP header.
+ *
+ * @author Janni Kovacs
+ */
+public class Caller {
+
+ private final static String TAG = "LastFm.Caller";
+
+ private final static String PARAM_API_KEY = "api_key";
+
+ private final static String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
+
+ private static Caller mInstance = null;
+
+ private final String apiRootUrl = DEFAULT_API_ROOT;
+
+ private final String userAgent = "Apollo";
+
+ private Result lastResult;
+
+ /**
+ * @param context The {@link Context} to use
+ */
+ private Caller(final Context context) {
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class
+ */
+ public final static synchronized Caller getInstance(final Context context) {
+ if (mInstance == null) {
+ mInstance = new Caller(context.getApplicationContext());
+ }
+ return mInstance;
+ }
+
+ /**
+ * @param method
+ * @param apiKey
+ * @param params
+ * @return
+ * @throws CallException
+ */
+ public Result call(final String method, final String apiKey, final String... params) {
+ return call(method, apiKey, map(params));
+ }
+
+ /**
+ * Performs the web-service call. If the <code>session</code> parameter is
+ * <code>non-null</code> then an authenticated call is made. If it's
+ * <code>null</code> then an unauthenticated call is made.<br/>
+ * The <code>apiKey</code> parameter is always required, even when a valid
+ * session is passed to this method.
+ *
+ * @param method The method to call
+ * @param apiKey A Last.fm API key
+ * @param params Parameters
+ * @param session A Session instance or <code>null</code>
+ * @return the result of the operation
+ */
+ public Result call(final String method, final String apiKey, Map<String, String> params) {
+ params = new WeakHashMap<String, String>(params);
+ InputStream inputStream = null;
+
+ // no entry in cache, load from web
+ if (inputStream == null) {
+ // fill parameter map with apiKey and session info
+ params.put(PARAM_API_KEY, apiKey);
+ try {
+ final HttpURLConnection urlConnection = openPostConnection(method, params);
+ inputStream = getInputStreamFromConnection(urlConnection);
+
+ if (inputStream == null) {
+ lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
+ urlConnection.getResponseMessage());
+ return lastResult;
+ }
+ } catch (final IOException ioEx) {
+ // We will assume that the server is not ready
+ Log.e(TAG, "Failed to download data", ioEx);
+ lastResult = Result.createHttpErrorResult(HttpStatus.SC_SERVICE_UNAVAILABLE,
+ ioEx.getLocalizedMessage());
+ return lastResult;
+ }
+ }
+
+ try {
+ final Result result = createResultFromInputStream(inputStream);
+ lastResult = result;
+ } catch (final IOException ioEx) {
+ Log.e(TAG, "Failed to read document", ioEx);
+ lastResult = new Result(ioEx.getLocalizedMessage());
+ } catch (final SAXException saxEx) {
+ Log.e(TAG, "Failed to parse document", saxEx);
+ lastResult = new Result(saxEx.getLocalizedMessage());
+ }
+ return lastResult;
+ }
+
+ /**
+ * Creates a new {@link HttpURLConnection}, sets the proxy, if available,
+ * and sets the User-Agent property.
+ *
+ * @param url URL to connect to
+ * @return a new connection.
+ * @throws IOException if an I/O exception occurs.
+ */
+ public HttpURLConnection openConnection(final String url) throws IOException {
+ final URL u = new URL(url);
+ HttpURLConnection urlConnection;
+ urlConnection = (HttpURLConnection)u.openConnection();
+ urlConnection.setRequestProperty("User-Agent", userAgent);
+ urlConnection.setUseCaches(true);
+ return urlConnection;
+ }
+
+ /**
+ * @param method
+ * @param params
+ * @return
+ * @throws IOException
+ */
+ private HttpURLConnection openPostConnection(final String method,
+ final Map<String, String> params) throws IOException {
+ final HttpURLConnection urlConnection = openConnection(apiRootUrl);
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setDoOutput(true);
+ urlConnection.setUseCaches(true);
+ final OutputStream outputStream = urlConnection.getOutputStream();
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
+ final String post = buildPostBody(method, params);
+ writer.write(post);
+ writer.close();
+ return urlConnection;
+ }
+
+ /**
+ * @param connection
+ * @return
+ * @throws IOException
+ */
+ private InputStream getInputStreamFromConnection(final HttpURLConnection connection)
+ throws IOException {
+ final int responseCode = connection.getResponseCode();
+
+ if (responseCode == HttpURLConnection.HTTP_FORBIDDEN
+ || responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
+ return connection.getErrorStream();
+ } else if (responseCode == HttpURLConnection.HTTP_OK) {
+ return connection.getInputStream();
+ }
+
+ return null;
+ }
+
+ /**
+ * @param inputStream
+ * @return
+ * @throws SAXException
+ * @throws IOException
+ */
+ private Result createResultFromInputStream(final InputStream inputStream) throws SAXException,
+ IOException {
+ final Document document = newDocumentBuilder().parse(
+ new InputSource(new InputStreamReader(inputStream, "UTF-8")));
+ final Element root = document.getDocumentElement(); // lfm element
+ final String statusString = root.getAttribute("status");
+ final Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
+ if (status == Status.FAILED) {
+ final Element errorElement = (Element)root.getElementsByTagName("error").item(0);
+ final int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
+ final String message = errorElement.getTextContent();
+ return Result.createRestErrorResult(errorCode, message);
+ } else {
+ return Result.createOkResult(document);
+ }
+ }
+
+ /**
+ * @return
+ */
+ private DocumentBuilder newDocumentBuilder() {
+ try {
+ final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ return builderFactory.newDocumentBuilder();
+ } catch (final ParserConfigurationException e) {
+ // better never happens
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @param method
+ * @param params
+ * @param strings
+ * @return
+ */
+ private String buildPostBody(final String method, final Map<String, String> params,
+ final String... strings) {
+ final StringBuilder builder = new StringBuilder(100);
+ builder.append("method=");
+ builder.append(method);
+ builder.append('&');
+ for (final Iterator<Entry<String, String>> it = params.entrySet().iterator(); it.hasNext();) {
+ final Entry<String, String> entry = it.next();
+ builder.append(entry.getKey());
+ builder.append('=');
+ builder.append(encode(entry.getValue()));
+ if (it.hasNext() || strings.length > 0) {
+ builder.append('&');
+ }
+ }
+ int count = 0;
+ for (final String string : strings) {
+ builder.append(count % 2 == 0 ? string : encode(string));
+ count++;
+ if (count != strings.length) {
+ if (count % 2 == 0) {
+ builder.append('&');
+ } else {
+ builder.append('=');
+ }
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/DomElement.java b/src/com/cyngn/eleven/lastfm/DomElement.java
new file mode 100644
index 0000000..bd26351
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/DomElement.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <code>DomElement</code> wraps around an {@link Element} and provides
+ * convenience methods.
+ *
+ * @author Janni Kovacs
+ */
+public class DomElement {
+ private final Element e;
+
+ /**
+ * Creates a new wrapper around the given {@link Element}.
+ *
+ * @param elem An w3c Element
+ */
+ public DomElement(final Element elem) {
+ e = elem;
+ }
+
+ /**
+ * @return the original Element
+ */
+ public Element getElement() {
+ return e;
+ }
+
+ /**
+ * Tests if this element has an attribute with the specified name.
+ *
+ * @param name Name of the attribute.
+ * @return <code>true</code> if this element has an attribute with the
+ * specified name.
+ */
+ public boolean hasAttribute(final String name) {
+ return e.hasAttribute(name);
+ }
+
+ /**
+ * Returns the attribute value to a given attribute name or
+ * <code>null</code> if the attribute doesn't exist.
+ *
+ * @param name The attribute's name
+ * @return Attribute value or <code>null</code>
+ */
+ public String getAttribute(final String name) {
+ return e.hasAttribute(name) ? e.getAttribute(name) : null;
+ }
+
+ /**
+ * @return the text content of the element
+ */
+ public String getText() {
+ // XXX e.getTextContent() doesn't exsist under Android (Lukasz
+ // Wisniewski)
+ // / getTextContent() is now available in at least Android 2.2 if not
+ // earlier, so we'll keep using that
+ // return e.hasChildNodes() ? e.getFirstChild().getNodeValue() : null;
+ return e.getTextContent();
+ }
+
+ /**
+ * Checks if this element has a child element with the given name.
+ *
+ * @param name The child's name
+ * @return <code>true</code> if this element has a child element with the
+ * given name
+ */
+ public boolean hasChild(final String name) {
+ final NodeList list = e.getElementsByTagName(name);
+ for (int i = 0, j = list.getLength(); i < j; i++) {
+ final Node item = list.item(i);
+ if (item.getParentNode() == e) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the child element with the given name or <code>null</code> if it
+ * doesn't exist.
+ *
+ * @param name The child's name
+ * @return the child element or <code>null</code>
+ */
+ public DomElement getChild(final String name) {
+ final NodeList list = e.getElementsByTagName(name);
+ if (list.getLength() == 0) {
+ return null;
+ }
+ for (int i = 0, j = list.getLength(); i < j; i++) {
+ final Node item = list.item(i);
+ if (item.getParentNode() == e) {
+ return new DomElement((Element)item);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the text content of a child node with the given name. If no such
+ * child exists or the child does not have text content, <code>null</code>
+ * is returned.
+ *
+ * @param name The child's name
+ * @return the child's text content or <code>null</code>
+ */
+ public String getChildText(final String name) {
+ final DomElement child = getChild(name);
+ return child != null ? child.getText() : null;
+ }
+
+ /**
+ * @return all children of this element
+ */
+ public List<DomElement> getChildren() {
+ return getChildren("*");
+ }
+
+ /**
+ * Returns all children of this element with the given tag name.
+ *
+ * @param name The children's tag name
+ * @return all matching children
+ */
+ public List<DomElement> getChildren(final String name) {
+ final List<DomElement> l = new ArrayList<DomElement>();
+ final NodeList list = e.getElementsByTagName(name);
+ for (int i = 0; i < list.getLength(); i++) {
+ final Node node = list.item(i);
+ if (node.getParentNode() == e) {
+ l.add(new DomElement((Element)node));
+ }
+ }
+ return l;
+ }
+
+ /**
+ * Returns this element's tag name.
+ *
+ * @return the tag name
+ */
+ public String getTagName() {
+ return e.getTagName();
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/Image.java b/src/com/cyngn/eleven/lastfm/Image.java
new file mode 100644
index 0000000..84ba206
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/Image.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.Locale;
+
+/**
+ * An <code>Image</code> contains metadata and URLs for an artist's image.
+ * Metadata contains title, votes, format and other. Images are available in
+ * various sizes, see {@link ImageSize} for all sizes.
+ *
+ * @author Janni Kovacs
+ * @see ImageSize
+ * @see Artist#getImages(String, String)
+ */
+public class Image extends ImageHolder {
+
+ final static ItemFactory<Image> FACTORY = new ImageFactory();
+
+ private String url;
+
+ private Image() {
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ private static class ImageFactory implements ItemFactory<Image> {
+ @Override
+ public Image createItemFromElement(final DomElement element) {
+ final Image i = new Image();
+ i.url = element.getChildText("url");
+ loadImages(i, element);
+ return i;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/ImageHolder.java b/src/com/cyngn/eleven/lastfm/ImageHolder.java
new file mode 100644
index 0000000..8b2b11e
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/ImageHolder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Abstract superclass for all items that may contain images (such as
+ * {@link Artist}s, {@link Album}s or {@link Track}s).
+ *
+ * @author Janni Kovacs
+ */
+public abstract class ImageHolder {
+
+ protected Map<ImageSize, String> imageUrls = new HashMap<ImageSize, String>();
+
+ /**
+ * Returns a Set of all {@link ImageSize}s available.
+ *
+ * @return all sizes
+ */
+ public Set<ImageSize> availableSizes() {
+ return imageUrls.keySet();
+ }
+
+ /**
+ * Returns the URL of the image in the specified size, or <code>null</code>
+ * if not available.
+ *
+ * @param size The preferred size
+ * @return an image URL
+ */
+ public String getImageURL(final ImageSize size) {
+ return imageUrls.get(size);
+ }
+
+ /**
+ * @param holder
+ * @param element
+ */
+ protected static void loadImages(final ImageHolder holder, final DomElement element) {
+ final Collection<DomElement> images = element.getChildren("image");
+ for (final DomElement image : images) {
+ final String attribute = image.getAttribute("size");
+ ImageSize size = null;
+ if (attribute == null) {
+ size = ImageSize.UNKNOWN;
+ } else {
+ try {
+ size = ImageSize.valueOf(attribute.toUpperCase(Locale.ENGLISH));
+ } catch (final IllegalArgumentException e) {
+ // if they suddenly again introduce a new image size
+ }
+ }
+ if (size != null) {
+ holder.imageUrls.put(size, image.getText());
+ }
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/ImageSize.java b/src/com/cyngn/eleven/lastfm/ImageSize.java
new file mode 100644
index 0000000..6f05f22
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/ImageSize.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+/**
+ * @author Janni Kovacs
+ */
+public enum ImageSize {
+
+ SMALL, MEDIUM, LARGE, EXTRALARGE, MEGA, UNKNOWN
+
+}
diff --git a/src/com/cyngn/eleven/lastfm/ItemFactory.java b/src/com/cyngn/eleven/lastfm/ItemFactory.java
new file mode 100644
index 0000000..5e699fd
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/ItemFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+/**
+ * An <code>ItemFactory</code> can be used to instantiate a value object - such
+ * as Artist, Album, Track, Tag - from an XML element. Use the
+ * {@link ItemFactoryBuilder} to obtain item factories for a specific type.
+ *
+ * @author Janni Kovacs
+ * @see com.cyngn.eleven.lastfm.api.ItemFactoryBuilder
+ * @see ResponseBuilder
+ */
+interface ItemFactory<T> {
+
+ /**
+ * Create a new instance of the type <code>T</code>, based on the passed
+ * {@link DomElement}.
+ *
+ * @param element the XML element
+ * @return a new object
+ */
+ public T createItemFromElement(DomElement element);
+
+}
diff --git a/src/com/cyngn/eleven/lastfm/ItemFactoryBuilder.java b/src/com/cyngn/eleven/lastfm/ItemFactoryBuilder.java
new file mode 100644
index 0000000..bdf2211
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/ItemFactoryBuilder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The <code>ItemFactoryBuilder</code> can be used to obtain {@link ItemFactory
+ * ItemFactories} for a specific type.
+ *
+ * @author Janni Kovacs
+ * @see ItemFactory
+ */
+final class ItemFactoryBuilder {
+
+ private final static ItemFactoryBuilder INSTANCE = new ItemFactoryBuilder();
+
+ @SuppressWarnings("rawtypes")
+ private final Map<Class, ItemFactory> factories = new HashMap<Class, ItemFactory>();
+
+ private ItemFactoryBuilder() {
+ // register default factories
+ addItemFactory(Album.class, Album.FACTORY);
+ addItemFactory(Artist.class, Artist.FACTORY);
+ addItemFactory(Image.class, Image.FACTORY);
+ }
+
+ /**
+ * Retrieve the instance of the <code>ItemFactoryBuilder</code>.
+ *
+ * @return the instance
+ */
+ public static ItemFactoryBuilder getFactoryBuilder() {
+ return INSTANCE;
+ }
+
+ /**
+ * @param <T>
+ * @param itemClass
+ * @param factory
+ */
+ public <T> void addItemFactory(final Class<T> itemClass, final ItemFactory<T> factory) {
+ factories.put(itemClass, factory);
+ }
+
+ /**
+ * Retrieves an {@link ItemFactory} for the given type, or <code>null</code>
+ * if no such factory was registered.
+ *
+ * @param itemClass the type's Class object
+ * @return the <code>ItemFactory</code> or <code>null</code>
+ */
+ @SuppressWarnings("unchecked")
+ public <T> ItemFactory<T> getItemFactory(final Class<T> itemClass) {
+ return factories.get(itemClass);
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/MapUtilities.java b/src/com/cyngn/eleven/lastfm/MapUtilities.java
new file mode 100644
index 0000000..3056f35
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/MapUtilities.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.Map;
+
+/**
+ * Utility class to perform various operations on Maps.
+ *
+ * @author Adrian Woodhead
+ */
+public final class MapUtilities {
+
+ private MapUtilities() {
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not null.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if null will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final String value) {
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not null.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if null will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final Integer value) {
+ if (value != null) {
+ map.put(key, value.toString());
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not -1.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if -1 will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key, final int value) {
+ if (value != -1) {
+ map.put(key, Integer.toString(value));
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not -1.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if -1 will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final double value) {
+ if (value != -1) {
+ map.put(key, Double.toString(value));
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/MusicEntry.java b/src/com/cyngn/eleven/lastfm/MusicEntry.java
new file mode 100644
index 0000000..557cd0d
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/MusicEntry.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+/**
+ * <code>MusicEntry</code> is the abstract superclass for {@link Track},
+ * {@link Artist} and {@link Album}. It encapsulates data and provides methods
+ * used in all subclasses, for example: name, playcount, images and more.
+ *
+ * @author Janni Kovacs
+ */
+public abstract class MusicEntry extends ImageHolder {
+
+ protected String name;
+
+ protected String url;
+
+ private String wikiSummary;
+
+ protected MusicEntry(final String name, final String url) {
+ this.name = name;
+ this.url = url;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getWikiSummary() {
+ return wikiSummary;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "[" + "name='" + name + '\'' + ", url='" + url
+ + '\'' + ']';
+ }
+
+ /**
+ * Loads all generic information from an XML <code>DomElement</code> into
+ * the given <code>MusicEntry</code> instance, i.e. the following tags:<br/>
+ * <ul>
+ * <li>playcount/plays</li>
+ * <li>listeners</li>
+ * <li>streamable</li>
+ * <li>name</li>
+ * <li>url</li>
+ * <li>mbid</li>
+ * <li>image</li>
+ * <li>tags</li>
+ * </ul>
+ *
+ * @param entry An entry
+ * @param element XML source element
+ */
+ protected static void loadStandardInfo(final MusicEntry entry, final DomElement element) {
+ // copy
+ entry.name = element.getChildText("name");
+ entry.url = element.getChildText("url");
+ // wiki
+ DomElement wiki = element.getChild("bio");
+ if (wiki == null) {
+ wiki = element.getChild("wiki");
+ }
+ if (wiki != null) {
+ entry.wikiSummary = wiki.getChildText("summary");
+ }
+ // images
+ ImageHolder.loadImages(entry, element);
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/PaginatedResult.java b/src/com/cyngn/eleven/lastfm/PaginatedResult.java
new file mode 100644
index 0000000..4872abe
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/PaginatedResult.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * A <code>PaginatedResult</code> is returned by methods which result set might
+ * be so large that it needs to be paginated. Each <code>PaginatedResult</code>
+ * contains the total number of result pages, the current page and a
+ * <code>Collection</code> of entries for the current page.
+ *
+ * @author Janni Kovacs
+ */
+public class PaginatedResult<T> implements Iterable<T> {
+
+ private final int page;
+
+ private final int totalPages;
+
+ public final Collection<T> pageResults;
+
+ /**
+ * @param page
+ * @param totalPages
+ * @param pageResults
+ */
+ PaginatedResult(final int page, final int totalPages, final Collection<T> pageResults) {
+ this.page = page;
+ this.totalPages = totalPages;
+ this.pageResults = pageResults;
+ }
+
+ /**
+ * Returns the page number of this result.
+ *
+ * @return page number
+ */
+ public int getPage() {
+ return page;
+ }
+
+ /**
+ * Returns the total number of pages available.
+ *
+ * @return total pages
+ */
+ public int getTotalPages() {
+ return totalPages;
+ }
+
+ /**
+ * Returns <code>true</code> if this Result contains no elements, which is
+ * the case for service calls that would have returned a
+ * <code>PaginatedResult</code> but fail.
+ *
+ * @return <code>true</code> if this result contains no elements
+ */
+ public boolean isEmpty() {
+ return pageResults == null || pageResults.isEmpty();
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return pageResults.iterator();
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/ResponseBuilder.java b/src/com/cyngn/eleven/lastfm/ResponseBuilder.java
new file mode 100644
index 0000000..b0ceb90
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/ResponseBuilder.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * This utility class can be used to generically generate Result objects
+ * (usually Lists or {@link PaginatedResult}s) from an XML response using
+ * {@link ItemFactory ItemFactories}.
+ *
+ * @author Janni Kovacs
+ */
+public final class ResponseBuilder {
+
+ private ResponseBuilder() {
+ }
+
+ /**
+ * @param <T>
+ * @param itemClass
+ * @return
+ */
+ private static <T> ItemFactory<T> getItemFactory(final Class<T> itemClass) {
+ return ItemFactoryBuilder.getFactoryBuilder().getItemFactory(itemClass);
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final Result result, final Class<T> itemClass) {
+ return buildCollection(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final Result result,
+ final ItemFactory<T> factory) {
+ if (!result.isSuccessful()) {
+ return Collections.emptyList();
+ }
+ return buildCollection(result.getContentElement(), factory);
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param itemClass
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final DomElement element,
+ final Class<T> itemClass) {
+ return buildCollection(element, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param factory
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final DomElement element,
+ final ItemFactory<T> factory) {
+ if (element == null) {
+ return Collections.emptyList();
+ }
+ final Collection<DomElement> children = element.getChildren();
+ final Collection<T> items = new ArrayList<T>(children.size());
+ for (final DomElement child : children) {
+ items.add(factory.createItemFromElement(child));
+ }
+ return items;
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final Result result,
+ final Class<T> itemClass) {
+ return buildPaginatedResult(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final Result result,
+ final ItemFactory<T> factory) {
+ if (result != null) {
+ if (!result.isSuccessful()) {
+ return new PaginatedResult<T>(0, 0, Collections.<T> emptyList());
+ }
+
+ final DomElement contentElement = result.getContentElement();
+ return buildPaginatedResult(contentElement, contentElement, factory);
+ }
+ return null;
+ }
+
+ /**
+ * @param <T>
+ * @param contentElement
+ * @param childElement
+ * @param itemClass
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final DomElement contentElement,
+ final DomElement childElement, final Class<T> itemClass) {
+ return buildPaginatedResult(contentElement, childElement, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param contentElement
+ * @param childElement
+ * @param factory
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final DomElement contentElement,
+ final DomElement childElement, final ItemFactory<T> factory) {
+ final Collection<T> items = buildCollection(childElement, factory);
+
+ String totalPagesAttribute = contentElement.getAttribute("totalPages");
+ if (totalPagesAttribute == null) {
+ totalPagesAttribute = contentElement.getAttribute("totalpages");
+ }
+
+ final int page = Integer.parseInt(contentElement.getAttribute("page"));
+ final int totalPages = Integer.parseInt(totalPagesAttribute);
+
+ return new PaginatedResult<T>(page, totalPages, items);
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> T buildItem(final Result result, final Class<T> itemClass) {
+ return buildItem(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> T buildItem(final Result result, final ItemFactory<T> factory) {
+ if (!result.isSuccessful()) {
+ return null;
+ }
+ return buildItem(result.getContentElement(), factory);
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param itemClass
+ * @return
+ */
+ public static <T> T buildItem(final DomElement element, final Class<T> itemClass) {
+ return buildItem(element, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param factory
+ * @return
+ */
+ private static <T> T buildItem(final DomElement element, final ItemFactory<T> factory) {
+ return factory.createItemFromElement(element);
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/Result.java b/src/com/cyngn/eleven/lastfm/Result.java
new file mode 100644
index 0000000..824e204
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/Result.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import org.w3c.dom.Document;
+
+/**
+ * The <code>Result</code> class contains the response sent by the server, i.e.
+ * the status (either ok or failed), an error code and message if failed and the
+ * xml response sent by the server.
+ *
+ * @author Janni Kovacs
+ */
+public class Result {
+
+ public enum Status {
+ OK, FAILED
+ }
+
+ protected Status status;
+
+ protected String errorMessage = null;
+
+ protected int errorCode = -1;
+
+ protected int httpErrorCode = -1;
+
+ protected Document resultDocument;
+
+ /**
+ * @param resultDocument
+ */
+ protected Result(final Document resultDocument) {
+ status = Status.OK;
+ this.resultDocument = resultDocument;
+ }
+
+ /**
+ * @param errorMessage
+ */
+ protected Result(final String errorMessage) {
+ status = Status.FAILED;
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * @param resultDocument
+ * @return
+ */
+ static Result createOkResult(final Document resultDocument) {
+ return new Result(resultDocument);
+ }
+
+ /**
+ * @param httpErrorCode
+ * @param errorMessage
+ * @return
+ */
+ static Result createHttpErrorResult(final int httpErrorCode, final String errorMessage) {
+ final Result r = new Result(errorMessage);
+ r.httpErrorCode = httpErrorCode;
+ return r;
+ }
+
+ /**
+ * @param errorCode
+ * @param errorMessage
+ * @return
+ */
+ static Result createRestErrorResult(final int errorCode, final String errorMessage) {
+ final Result r = new Result(errorMessage);
+ r.errorCode = errorCode;
+ return r;
+ }
+
+ /**
+ * Returns if the operation was successful. Same as
+ * <code>getStatus() == Status.OK</code>.
+ *
+ * @return <code>true</code> if the operation was successful
+ */
+ public boolean isSuccessful() {
+ return status == Status.OK;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public int getHttpErrorCode() {
+ return httpErrorCode;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Document getResultDocument() {
+ return resultDocument;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public DomElement getContentElement() {
+ if (!isSuccessful()) {
+ return null;
+ }
+ return new DomElement(resultDocument.getDocumentElement()).getChild("*");
+ }
+
+ @Override
+ public String toString() {
+ return "Result[isSuccessful=" + isSuccessful() + ", errorCode=" + errorCode
+ + ", httpErrorCode=" + httpErrorCode + ", errorMessage=" + errorMessage
+ + ", status=" + status + "]";
+ }
+}
diff --git a/src/com/cyngn/eleven/lastfm/StringUtilities.java b/src/com/cyngn/eleven/lastfm/StringUtilities.java
new file mode 100644
index 0000000..982ea5f
--- /dev/null
+++ b/src/com/cyngn/eleven/lastfm/StringUtilities.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.cyngn.eleven.lastfm;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Utilitiy class with methods to calculate an md5 hash and to encode URLs.
+ *
+ * @author Janni Kovacs
+ */
+public final class StringUtilities {
+
+ private static MessageDigest mDigest;
+
+ private final static Pattern MD5_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");
+
+ static {
+ try {
+ mDigest = MessageDigest.getInstance("MD5");
+ } catch (final NoSuchAlgorithmException ignored) {
+ }
+ }
+
+ /**
+ * Returns a 32 chararacter hexadecimal representation of an MD5 hash of the
+ * given String.
+ *
+ * @param s the String to hash
+ * @return the md5 hash
+ */
+ public final static String md5(final String s) {
+ try {
+ final byte[] mBytes = mDigest.digest(s.getBytes("UTF-8"));
+ final StringBuilder mBuilder = new StringBuilder(32);
+ for (final byte aByte : mBytes) {
+ final String mHex = Integer.toHexString(aByte & 0xFF);
+ if (mHex.length() == 1) {
+ mBuilder.append('0');
+ }
+ mBuilder.append(mHex);
+ }
+ return mBuilder.toString();
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * URL Encodes the given String <code>s</code> using the UTF-8 character
+ * encoding.
+ *
+ * @param s a String
+ * @return url encoded string
+ */
+ public static String encode(final String s) {
+ if (s == null) {
+ return null;
+ }
+ try {
+ return URLEncoder.encode(s, "UTF-8");
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Decodes an URL encoded String <code>s</code> using the UTF-8 character
+ * encoding.
+ *
+ * @param s an encoded String
+ * @return the decoded String
+ */
+ public static String decode(final String s) {
+ if (s == null) {
+ return null;
+ }
+ try {
+ return URLDecoder.decode(s, "UTF-8");
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Creates a Map out of an array with Strings.
+ *
+ * @param strings input strings, key-value alternating
+ * @return a parameter map
+ */
+ public static Map<String, String> map(final String... strings) {
+ if (strings.length % 2 != 0) {
+ throw new IllegalArgumentException("strings.length % 2 != 0");
+ }
+ final Map<String, String> sMap = new HashMap<String, String>();
+ for (int i = 0; i < strings.length; i += 2) {
+ sMap.put(strings[i], strings[i + 1]);
+ }
+ return sMap;
+ }
+
+ /**
+ * Strips all characters from a String, that might be invalid to be used in
+ * file names. By default <tt>: / \ < > | ? " *</tt> are all replaced by
+ * <tt>-</tt>. Note that this is no guarantee that the returned name will be
+ * definately valid.
+ *
+ * @param s the String to clean up
+ * @return the cleaned up String
+ */
+ public static String cleanUp(final String s) {
+ return s.replaceAll("[*:/\\\\?|<>\"]", "-");
+ }
+
+ /**
+ * Tests if the given string <i>might</i> already be a 32-char md5 string.
+ *
+ * @param s String to test
+ * @return <code>true</code> if the given String might be a md5 string
+ */
+ public static boolean isMD5(final String s) {
+ return s.length() == 32 && MD5_PATTERN.matcher(s).matches();
+ }
+
+ /**
+ * Converts a Last.fm boolean result string to a boolean.
+ *
+ * @param resultString A Last.fm boolean result string.
+ * @return <code>true</code> if the given String represents a true,
+ * <code>false</code> otherwise.
+ */
+ public static boolean convertToBoolean(final String resultString) {
+ return "1".equals(resultString);
+ }
+
+ /**
+ * Converts from a boolean to a Last.fm boolean result string.
+ *
+ * @param value A boolean value.
+ * @return A string representing a Last.fm boolean.
+ */
+ public static String convertFromBoolean(final boolean value) {
+ if (value) {
+ return "1";
+ } else {
+ return "0";
+ }
+ }
+
+}
diff --git a/src/com/cyngn/eleven/loaders/AlbumLoader.java b/src/com/cyngn/eleven/loaders/AlbumLoader.java
new file mode 100644
index 0000000..f6f80f1
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/AlbumLoader.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI} and return
+ * the albums on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>AlbumLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public AlbumLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeAlbumCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final long id = mCursor.getLong(0);
+
+ // Copy the album name
+ final String albumName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the number of songs
+ final int songCount = mCursor.getInt(3);
+
+ // Copy the release year
+ final String year = mCursor.getString(4);
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCount, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the album query.
+ */
+ public static final Cursor makeAlbumCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AlbumColumns.ALBUM,
+ /* 2 */
+ AlbumColumns.ARTIST,
+ /* 3 */
+ AlbumColumns.NUMBER_OF_SONGS,
+ /* 4 */
+ AlbumColumns.FIRST_YEAR
+ }, null, null, PreferenceUtils.getInstance(context).getAlbumSortOrder());
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/AlbumSongLoader.java b/src/com/cyngn/eleven/loaders/AlbumSongLoader.java
new file mode 100644
index 0000000..81e9c87
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/AlbumSongLoader.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the Song for a particular album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the album the songs belong to.
+ */
+ private final Long mAlbumID;
+
+ /**
+ * Constructor of <code>AlbumSongHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param albumId The Id of the album the songs belong to.
+ */
+ public AlbumSongLoader(final Context context, final Long albumId) {
+ super(context);
+ mAlbumID = albumId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeAlbumSongCursor(getContext(), mAlbumID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Make the duration label
+ final int seconds = (int) (duration / 1000);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, seconds);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param albumId The Id of the album the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeAlbumSongCursor(final Context context, final Long albumId) {
+ // Match the songs up with the artist
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''");
+ selection.append(" AND " + AudioColumns.ALBUM_ID + "=" + albumId);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ }, selection.toString(), null,
+ PreferenceUtils.getInstance(context).getAlbumSongSortOrder());
+ }
+
+}
diff --git a/src/com/cyngn/eleven/loaders/ArtistAlbumLoader.java b/src/com/cyngn/eleven/loaders/ArtistAlbumLoader.java
new file mode 100644
index 0000000..d977751
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/ArtistAlbumLoader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Artists.Albums} and return the albums
+ * for a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the artist the albums belong to.
+ */
+ private final Long mArtistID;
+
+ /**
+ * Constructor of <code>ArtistAlbumHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the albums belong to.
+ */
+ public ArtistAlbumLoader(final Context context, final Long artistId) {
+ super(context);
+ mArtistID = artistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistAlbumCursor(getContext(), mArtistID);
+ // Gather the dataS
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final long id = mCursor.getLong(0);
+
+ // Copy the album name
+ final String albumName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the number of songs
+ final int songCount = mCursor.getInt(3);
+
+ // Copy the release year
+ final String year = mCursor.getString(4);
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCount, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the albums belong to.
+ */
+ public static final Cursor makeArtistAlbumCursor(final Context context, final Long artistId) {
+ return context.getContentResolver().query(
+ MediaStore.Audio.Artists.Albums.getContentUri("external", artistId), new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AlbumColumns.ALBUM,
+ /* 2 */
+ AlbumColumns.ARTIST,
+ /* 3 */
+ AlbumColumns.NUMBER_OF_SONGS,
+ /* 4 */
+ AlbumColumns.FIRST_YEAR
+ }, null, null, PreferenceUtils.getInstance(context).getArtistAlbumSortOrder());
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/ArtistLoader.java b/src/com/cyngn/eleven/loaders/ArtistLoader.java
new file mode 100644
index 0000000..4956b90
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/ArtistLoader.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.ArtistColumns;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI} and
+ * return the artists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistLoader extends WrappedAsyncTaskLoader<List<Artist>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Artist> mArtistsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>ArtistLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public ArtistLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Artist> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the artist id
+ final long id = mCursor.getLong(0);
+
+ // Copy the artist name
+ final String artistName = mCursor.getString(1);
+
+ // Copy the number of albums
+ final int albumCount = mCursor.getInt(2);
+
+ // Copy the number of songs
+ final int songCount = mCursor.getInt(3);
+
+ // Create a new artist
+ final Artist artist = new Artist(id, artistName, songCount, albumCount);
+
+ // Add everything up
+ mArtistsList.add(artist);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mArtistsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the artist query.
+ */
+ public static final Cursor makeArtistCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ ArtistColumns.ARTIST,
+ /* 2 */
+ ArtistColumns.NUMBER_OF_ALBUMS,
+ /* 3 */
+ ArtistColumns.NUMBER_OF_TRACKS
+ }, null, null, PreferenceUtils.getInstance(context).getArtistSortOrder());
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/ArtistSongLoader.java b/src/com/cyngn/eleven/loaders/ArtistSongLoader.java
new file mode 100644
index 0000000..8d32fbc
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/ArtistSongLoader.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the songs for a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the artist the songs belong to.
+ */
+ private final Long mArtistID;
+
+ /**
+ * Constructor of <code>ArtistSongLoader</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the songs belong to.
+ */
+ public ArtistSongLoader(final Context context, final Long artistId) {
+ super(context);
+ mArtistID = artistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistSongCursor(getContext(), mArtistID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeArtistSongCursor(final Context context, final Long artistId) {
+ // Match the songs up with the artist
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''");
+ selection.append(" AND " + AudioColumns.ARTIST_ID + "=" + artistId);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ }, selection.toString(), null,
+ PreferenceUtils.getInstance(context).getArtistSongSortOrder());
+ }
+
+}
diff --git a/src/com/cyngn/eleven/loaders/AsyncHandler.java b/src/com/cyngn/eleven/loaders/AsyncHandler.java
new file mode 100644
index 0000000..7788f35
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/AsyncHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Helper class for managing the background thread used to perform io operations
+ * and handle async broadcasts.
+ */
+public final class AsyncHandler {
+
+ private static final HandlerThread sHandlerThread = new HandlerThread("AsyncHandler");
+
+ private static final Handler sHandler;
+
+ static {
+ sHandlerThread.start();
+ sHandler = new Handler(sHandlerThread.getLooper());
+ }
+
+ /* This class is never initiated */
+ private AsyncHandler() {
+ }
+
+ /**
+ * @param r The {@link Runnable} to execute.
+ */
+ public static void post(final Runnable r) {
+ sHandler.post(r);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/loaders/FavoritesLoader.java b/src/com/cyngn/eleven/loaders/FavoritesLoader.java
new file mode 100644
index 0000000..e517d81
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/FavoritesLoader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.provider.FavoritesStore.FavoriteColumns;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query the {@link FavoritesStore} for the tracks marked as favorites.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoritesLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>FavoritesHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public FavoritesLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeFavoritesCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+
+ // Copy the song Id
+ final long id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ID));
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.SONGNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ARTISTNAME));
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ALBUMNAME));
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, -1);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the favorites query.
+ */
+ public static final Cursor makeFavoritesCursor(final Context context) {
+ return FavoritesStore
+ .getInstance(context)
+ .getReadableDatabase()
+ .query(FavoriteColumns.NAME,
+ new String[] {
+ FavoriteColumns.ID + " as _id", FavoriteColumns.ID,
+ FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ }, null, null, null, null, FavoriteColumns.PLAYCOUNT + " DESC");
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/GenreLoader.java b/src/com/cyngn/eleven/loaders/GenreLoader.java
new file mode 100644
index 0000000..7d102dd
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/GenreLoader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.GenresColumns;
+
+import com.cyngn.eleven.model.Genre;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI} and return
+ * the genres on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreLoader extends WrappedAsyncTaskLoader<List<Genre>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Genre> mGenreList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>GenreLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public GenreLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Genre> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeGenreCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the genre id
+ final long id = mCursor.getLong(0);
+
+ // Copy the genre name
+ final String name = mCursor.getString(1);
+
+ // Create a new genre
+ final Genre genre = new Genre(id, name);
+
+ // Add everything up
+ mGenreList.add(genre);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mGenreList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the genre query.
+ */
+ public static final Cursor makeGenreCursor(final Context context) {
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Genres.NAME + " != ''");
+ return context.getContentResolver().query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ GenresColumns.NAME
+ }, selection.toString(), null, MediaStore.Audio.Genres.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/GenreSongLoader.java b/src/com/cyngn/eleven/loaders/GenreSongLoader.java
new file mode 100644
index 0000000..f684f93
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/GenreSongLoader.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.MediaStore;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Genres.Members.EXTERNAL_CONTENT_URI}
+ * and return the songs for a particular genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the genre the songs belong to.
+ */
+ private final Long mGenreID;
+
+ /**
+ * Constructor of <code>GenreSongHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param genreID The Id of the genre the songs belong to.
+ */
+ public GenreSongLoader(final Context context, final Long genreId) {
+ super(context);
+ mGenreID = genreId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeGenreSongCursor(getContext(), mGenreID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the album name
+ final String album = mCursor.getString(2);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param genreId The Id of the genre the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeGenreSongCursor(final Context context, final Long genreId) {
+ // Match the songs up with the genre
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Genres.Members.IS_MUSIC + "=1");
+ selection.append(" AND " + MediaStore.Audio.Genres.Members.TITLE + "!=''"); //$NON-NLS-2$
+ return context.getContentResolver().query(
+ MediaStore.Audio.Genres.Members.getContentUri("external", genreId), new String[] {
+ /* 0 */
+ MediaStore.Audio.Genres.Members._ID,
+ /* 1 */
+ MediaStore.Audio.Genres.Members.TITLE,
+ /* 2 */
+ MediaStore.Audio.Genres.Members.ALBUM,
+ /* 3 */
+ MediaStore.Audio.Genres.Members.ARTIST,
+ /* 4 */
+ MediaStore.Audio.Genres.Members.DURATION
+ }, selection.toString(), null, MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/LastAddedLoader.java b/src/com/cyngn/eleven/loaders/LastAddedLoader.java
new file mode 100644
index 0000000..fc3b44d
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/LastAddedLoader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the Song the user added over the past four of weeks.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class LastAddedLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>LastAddedHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public LastAddedLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeLastAddedCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeLastAddedCursor(final Context context) {
+ final int fourWeeks = 4 * 3600 * 24 * 7;
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ selection.append(" AND " + MediaStore.Audio.Media.DATE_ADDED + ">"); //$NON-NLS-2$
+ selection.append(System.currentTimeMillis() / 1000 - fourWeeks);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ }, selection.toString(), null, MediaStore.Audio.Media.DATE_ADDED + " DESC");
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/NowPlayingCursor.java b/src/com/cyngn/eleven/loaders/NowPlayingCursor.java
new file mode 100644
index 0000000..2254ee8
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/NowPlayingCursor.java
@@ -0,0 +1,291 @@
+
+package com.cyngn.eleven.loaders;
+
+import static com.cyngn.eleven.utils.MusicUtils.mService;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.Arrays;
+
+/**
+ * A custom {@link Cursor} used to return the queue and allow for easy dragging
+ * and dropping of the items in it.
+ */
+@SuppressLint("NewApi")
+public class NowPlayingCursor extends AbstractCursor {
+
+ private static final String[] PROJECTION = new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ };
+
+ private final Context mContext;
+
+ private long[] mNowPlaying;
+
+ private long[] mCursorIndexes;
+
+ private int mSize;
+
+ private int mCurPos;
+
+ private Cursor mQueueCursor;
+
+ /**
+ * Constructor of <code>NowPlayingCursor</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public NowPlayingCursor(final Context context) {
+ mContext = context;
+ makeNowPlayingCursor();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ return mSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onMove(final int oldPosition, final int newPosition) {
+ if (oldPosition == newPosition) {
+ return true;
+ }
+
+ if (mNowPlaying == null || mCursorIndexes == null || newPosition >= mNowPlaying.length) {
+ return false;
+ }
+
+ final long id = mNowPlaying[newPosition];
+ final int cursorIndex = Arrays.binarySearch(mCursorIndexes, id);
+ mQueueCursor.moveToPosition(cursorIndex);
+ mCurPos = newPosition;
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getString(final int column) {
+ try {
+ return mQueueCursor.getString(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return "";
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public short getShort(final int column) {
+ return mQueueCursor.getShort(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getInt(final int column) {
+ try {
+ return mQueueCursor.getInt(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getLong(final int column) {
+ try {
+ return mQueueCursor.getLong(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getFloat(final int column) {
+ return mQueueCursor.getFloat(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public double getDouble(final int column) {
+ return mQueueCursor.getDouble(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getType(final int column) {
+ return mQueueCursor.getType(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isNull(final int column) {
+ return mQueueCursor.isNull(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getColumnNames() {
+ return PROJECTION;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public void deactivate() {
+ if (mQueueCursor != null) {
+ mQueueCursor.deactivate();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean requery() {
+ makeNowPlayingCursor();
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() {
+ try {
+ if (mQueueCursor != null) {
+ mQueueCursor.close();
+ mQueueCursor = null;
+ }
+ } catch (final Exception close) {
+ }
+ super.close();
+ };
+
+ /**
+ * Actually makes the queue
+ */
+ private void makeNowPlayingCursor() {
+ mQueueCursor = null;
+ mNowPlaying = MusicUtils.getQueue();
+ mSize = mNowPlaying.length;
+ if (mSize == 0) {
+ return;
+ }
+
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Media._ID + " IN (");
+ for (int i = 0; i < mSize; i++) {
+ selection.append(mNowPlaying[i]);
+ if (i < mSize - 1) {
+ selection.append(",");
+ }
+ }
+ selection.append(")");
+
+ mQueueCursor = mContext.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, PROJECTION, selection.toString(),
+ null, MediaStore.Audio.Media._ID);
+
+ if (mQueueCursor == null) {
+ mSize = 0;
+ return;
+ }
+
+ final int playlistSize = mQueueCursor.getCount();
+ mCursorIndexes = new long[playlistSize];
+ mQueueCursor.moveToFirst();
+ final int columnIndex = mQueueCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+ for (int i = 0; i < playlistSize; i++) {
+ mCursorIndexes[i] = mQueueCursor.getLong(columnIndex);
+ mQueueCursor.moveToNext();
+ }
+ mQueueCursor.moveToFirst();
+ mCurPos = -1;
+
+ int removed = 0;
+ for (int i = mNowPlaying.length - 1; i >= 0; i--) {
+ final long trackId = mNowPlaying[i];
+ final int cursorIndex = Arrays.binarySearch(mCursorIndexes, trackId);
+ if (cursorIndex < 0) {
+ removed += MusicUtils.removeTrack(trackId);
+ }
+ }
+ if (removed > 0) {
+ mNowPlaying = MusicUtils.getQueue();
+ mSize = mNowPlaying.length;
+ if (mSize == 0) {
+ mCursorIndexes = null;
+ return;
+ }
+ }
+ }
+
+ /**
+ * @param which The position to remove
+ * @return True if sucessfull, false othersise
+ */
+ public boolean removeItem(final int which) {
+ try {
+ if (mService.removeTracks(which, which) == 0) {
+ return false;
+ }
+ int i = which;
+ mSize--;
+ while (i < mSize) {
+ mNowPlaying[i] = mNowPlaying[i + 1];
+ i++;
+ }
+ onMove(-1, mCurPos);
+ } catch (final RemoteException ignored) {
+ }
+ return true;
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/PlaylistLoader.java b/src/com/cyngn/eleven/loaders/PlaylistLoader.java
new file mode 100644
index 0000000..3e34e75
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/PlaylistLoader.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.PlaylistsColumns;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Playlist;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI} and
+ * return the playlists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistLoader extends WrappedAsyncTaskLoader<List<Playlist>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Playlist> mPlaylistList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>PlaylistLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public PlaylistLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Playlist> loadInBackground() {
+ // Add the deafult playlits to the adapter
+ makeDefaultPlaylists();
+
+ // Create the Cursor
+ mCursor = makePlaylistCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the playlist id
+ final long id = mCursor.getLong(0);
+
+ // Copy the playlist name
+ final String name = mCursor.getString(1);
+
+ // Create a new playlist
+ final Playlist playlist = new Playlist(id, name);
+
+ // Add everything up
+ mPlaylistList.add(playlist);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mPlaylistList;
+ }
+
+ /* Adds the favorites and last added playlists */
+ private void makeDefaultPlaylists() {
+ final Resources resources = getContext().getResources();
+
+ /* Favorites list */
+ final Playlist favorites = new Playlist(-1,
+ resources.getString(R.string.playlist_favorites));
+ mPlaylistList.add(favorites);
+
+ /* Last added list */
+ final Playlist lastAdded = new Playlist(-2,
+ resources.getString(R.string.playlist_last_added));
+ mPlaylistList.add(lastAdded);
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the playlist query.
+ */
+ public static final Cursor makePlaylistCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ PlaylistsColumns.NAME
+ }, null, null, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/PlaylistSongLoader.java b/src/com/cyngn/eleven/loaders/PlaylistSongLoader.java
new file mode 100644
index 0000000..745d971
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/PlaylistSongLoader.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI} and
+ * return the songs for a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the playlist the songs belong to.
+ */
+ private final Long mPlaylistID;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ * @param playlistID The Id of the playlist the songs belong to.
+ */
+ public PlaylistSongLoader(final Context context, final Long playlistId) {
+ super(context);
+ mPlaylistID = playlistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makePlaylistSongCursor(getContext(), mPlaylistID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.TITLE));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.ARTIST));
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.ALBUM));
+
+ // Copy the duration
+ final long duration = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(AudioColumns.DURATION));
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @param playlistID The playlist the songs belong to.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makePlaylistSongCursor(final Context context, final Long playlistID) {
+ final StringBuilder mSelection = new StringBuilder();
+ mSelection.append(AudioColumns.IS_MUSIC + "=1");
+ mSelection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ return context.getContentResolver().query(
+ MediaStore.Audio.Playlists.Members.getContentUri("external", playlistID),
+ new String[] {
+ /* 0 */
+ MediaStore.Audio.Playlists.Members._ID,
+ /* 1 */
+ MediaStore.Audio.Playlists.Members.AUDIO_ID,
+ /* 2 */
+ AudioColumns.TITLE,
+ /* 3 */
+ AudioColumns.ARTIST,
+ /* 4 */
+ AudioColumns.ALBUM,
+ /* 5 */
+ AudioColumns.DURATION
+ }, mSelection.toString(), null,
+ MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/QueueLoader.java b/src/com/cyngn/eleven/loaders/QueueLoader.java
new file mode 100644
index 0000000..3c4491c
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/QueueLoader.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to return the current playlist or queue.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class QueueLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private NowPlayingCursor mCursor;
+
+ /**
+ * Constructor of <code>QueueLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public QueueLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = new NowPlayingCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeQueueCursor(final Context context) {
+ final Cursor cursor = new NowPlayingCursor(context);
+ return cursor;
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/RecentLoader.java b/src/com/cyngn/eleven/loaders/RecentLoader.java
new file mode 100644
index 0000000..781cc8f
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/RecentLoader.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.provider.RecentStore;
+import com.cyngn.eleven.provider.RecentStore.RecentStoreColumns;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link RecentStore} and return the last listened to albums.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>RecentLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public RecentLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeRecentCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final long id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ID));
+
+ // Copy the album name
+ final String albumName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ARTISTNAME));
+
+ // Copy the number of songs
+ final int songCount = mCursor.getInt(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMSONGCOUNT));
+
+ // Copy the release year
+ final String year = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMYEAR));
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCount, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the album query.
+ */
+ public static final Cursor makeRecentCursor(final Context context) {
+ return RecentStore
+ .getInstance(context)
+ .getReadableDatabase()
+ .query(RecentStoreColumns.NAME,
+ new String[] {
+ RecentStoreColumns.ID + " as id", RecentStoreColumns.ID,
+ RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.ALBUMSONGCOUNT, RecentStoreColumns.ALBUMYEAR,
+ RecentStoreColumns.TIMEPLAYED
+ }, null, null, null, null, RecentStoreColumns.TIMEPLAYED + " DESC");
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/SearchLoader.java b/src/com/cyngn/eleven/loaders/SearchLoader.java
new file mode 100644
index 0000000..d86b41a
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/SearchLoader.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SearchLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ * @param query The search query
+ */
+ public SearchLoader(final Context context, final String query) {
+ super(context);
+ // Create the Cursor
+ mCursor = makeSearchCursor(context, query);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ long id = -1;
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
+
+ // Check for a song Id
+ if (!TextUtils.isEmpty(songName)) {
+ id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
+ }
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+
+ // Check for a album Id
+ if (id < 0 && !TextUtils.isEmpty(album)) {
+ id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
+ }
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+
+ // Check for a artist Id
+ if (id < 0 && !TextUtils.isEmpty(artist)) {
+ id = mCursor.getLong(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists._ID));
+ }
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, -1);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * * @param context The {@link Context} to use.
+ *
+ * @param query The user's query.
+ * @return The {@link Cursor} used to perform the search.
+ */
+ public static final Cursor makeSearchCursor(final Context context, final String query) {
+ return context.getContentResolver().query(
+ Uri.parse("content://media/external/audio/search/fancy/" + Uri.encode(query)),
+ new String[] {
+ BaseColumns._ID, MediaStore.Audio.Media.MIME_TYPE,
+ MediaStore.Audio.Artists.ARTIST, MediaStore.Audio.Albums.ALBUM,
+ MediaStore.Audio.Media.TITLE, "data1", "data2" //$NON-NLS-2$
+ }, null, null, null);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/loaders/SongLoader.java b/src/com/cyngn/eleven/loaders/SongLoader.java
new file mode 100644
index 0000000..cb9e77f
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/SongLoader.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the songs on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public SongLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeSongCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final long id = mCursor.getLong(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final long duration = mCursor.getLong(4);
+
+ // Convert the duration into seconds
+ final int durationInSecs = (int) duration / 1000;
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationInSecs);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeSongCursor(final Context context) {
+ final StringBuilder mSelection = new StringBuilder();
+ mSelection.append(AudioColumns.IS_MUSIC + "=1");
+ mSelection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ }, mSelection.toString(), null,
+ PreferenceUtils.getInstance(context).getSongSortOrder());
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/WrappedAsyncTaskLoader.java b/src/com/cyngn/eleven/loaders/WrappedAsyncTaskLoader.java
new file mode 100644
index 0000000..50d3f27
--- /dev/null
+++ b/src/com/cyngn/eleven/loaders/WrappedAsyncTaskLoader.java
@@ -0,0 +1,70 @@
+
+package com.cyngn.eleven.loaders;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * <a href="http://code.google.com/p/android/issues/detail?id=14944">Issue
+ * 14944</a>
+ *
+ * @author Alexander Blom
+ */
+public abstract class WrappedAsyncTaskLoader<D> extends AsyncTaskLoader<D> {
+
+ private D mData;
+
+ /**
+ * Constructor of <code>WrappedAsyncTaskLoader</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public WrappedAsyncTaskLoader(Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void deliverResult(D data) {
+ if (!isReset()) {
+ this.mData = data;
+ super.deliverResult(data);
+ } else {
+ // An asynchronous query came in while the loader is stopped
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStartLoading() {
+ if (this.mData != null) {
+ deliverResult(this.mData);
+ } else if (takeContentChanged() || this.mData == null) {
+ forceLoad();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible
+ cancelLoad();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onReset() {
+ super.onReset();
+ // Ensure the loader is stopped
+ onStopLoading();
+ this.mData = null;
+ }
+}
diff --git a/src/com/cyngn/eleven/menu/BasePlaylistDialog.java b/src/com/cyngn/eleven/menu/BasePlaylistDialog.java
new file mode 100644
index 0000000..175239b
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/BasePlaylistDialog.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * A simple base class for the playlist dialogs.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public abstract class BasePlaylistDialog extends DialogFragment {
+
+ /* The actual dialog */
+ protected AlertDialog mPlaylistDialog;
+
+ /* Used to make new playlist names */
+ protected EditText mPlaylist;
+
+ /* The dialog save button */
+ protected Button mSaveButton;
+
+ /* The dialog prompt */
+ protected String mPrompt;
+
+ /* The default edit text text */
+ protected String mDefaultname;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ // Initialize the alert dialog
+ mPlaylistDialog = new AlertDialog.Builder(getActivity()).create();
+ // Initialize the edit text
+ mPlaylist = new EditText(getActivity());
+ // To show the "done" button on the soft keyboard
+ mPlaylist.setSingleLine(true);
+ // All caps
+ mPlaylist.setInputType(mPlaylist.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
+ // Set the save button action
+ mPlaylistDialog.setButton(Dialog.BUTTON_POSITIVE, getString(R.string.save),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ onSaveClick();
+ MusicUtils.refresh();
+ dialog.dismiss();
+ }
+ });
+ // Set the cancel button action
+ mPlaylistDialog.setButton(Dialog.BUTTON_NEGATIVE, getString(R.string.cancel),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ closeKeyboard();
+ MusicUtils.refresh();
+ dialog.dismiss();
+ }
+ });
+
+ mPlaylist.post(new Runnable() {
+
+ @Override
+ public void run() {
+ // Open up the soft keyboard
+ openKeyboard();
+ // Request focus to the edit text
+ mPlaylist.requestFocus();
+ // Select the playlist name
+ mPlaylist.selectAll();
+ };
+ });
+
+ initObjects(savedInstanceState);
+ mPlaylistDialog.setTitle(mPrompt);
+ mPlaylistDialog.setView(mPlaylist);
+ mPlaylist.setText(mDefaultname);
+ mPlaylist.setSelection(mDefaultname.length());
+ mPlaylist.addTextChangedListener(mTextWatcher);
+ mPlaylistDialog.show();
+ return mPlaylistDialog;
+ }
+
+ /**
+ * Opens the soft keyboard
+ */
+ protected void openKeyboard() {
+ final InputMethodManager mInputMethodManager = (InputMethodManager)getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mInputMethodManager.toggleSoftInputFromWindow(mPlaylist.getApplicationWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ }
+
+ /**
+ * Closes the soft keyboard
+ */
+ protected void closeKeyboard() {
+ final InputMethodManager mInputMethodManager = (InputMethodManager)getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mInputMethodManager.hideSoftInputFromWindow(mPlaylist.getWindowToken(), 0);
+ }
+
+ /**
+ * Simple {@link TextWatcher}
+ */
+ private final TextWatcher mTextWatcher = new TextWatcher() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ onTextChangedListener();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void afterTextChanged(final Editable s) {
+ /* Nothing to do */
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ /* Nothing to do */
+ }
+ };
+
+ /**
+ * Initializes the prompt and default name
+ */
+ public abstract void initObjects(Bundle savedInstanceState);
+
+ /**
+ * Called when the save button of our {@link AlertDialog} is pressed
+ */
+ public abstract void onSaveClick();
+
+ /**
+ * Called in our {@link TextWatcher} during a text change
+ */
+ public abstract void onTextChangedListener();
+
+}
diff --git a/src/com/cyngn/eleven/menu/CreateNewPlaylist.java b/src/com/cyngn/eleven/menu/CreateNewPlaylist.java
new file mode 100644
index 0000000..8c1757b
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/CreateNewPlaylist.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.format.Capitalize;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com) TODO - The playlist names are
+ * automatically capitalized to help when you want to play one via voice
+ * actions, but it really needs to work either way. As in, capitalized
+ * or not.
+ */
+public class CreateNewPlaylist extends BasePlaylistDialog {
+
+ // The playlist list
+ private long[] mPlaylistList = new long[] {};
+
+ /**
+ * @param list The list of tracks to add to the playlist
+ * @return A new instance of this dialog.
+ */
+ public static CreateNewPlaylist getInstance(final long[] list) {
+ final CreateNewPlaylist frag = new CreateNewPlaylist();
+ final Bundle args = new Bundle();
+ args.putLongArray("playlist_list", list);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outcicle) {
+ outcicle.putString("defaultname", mPlaylist.getText().toString());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void initObjects(final Bundle savedInstanceState) {
+ mPlaylistList = getArguments().getLongArray("playlist_list");
+ mDefaultname = savedInstanceState != null ? savedInstanceState.getString("defaultname")
+ : makePlaylistName();
+ if (mDefaultname == null) {
+ getDialog().dismiss();
+ return;
+ }
+ final String prromptformat = getString(R.string.create_playlist_prompt);
+ mPrompt = String.format(prromptformat, mDefaultname);
+ }
+
+ @Override
+ public void onSaveClick() {
+ final String playlistName = mPlaylist.getText().toString();
+ if (playlistName != null && playlistName.length() > 0) {
+ final int playlistId = (int)MusicUtils.getIdForPlaylist(getActivity(),
+ playlistName);
+ if (playlistId >= 0) {
+ MusicUtils.clearPlaylist(getActivity(), playlistId);
+ MusicUtils.addToPlaylist(getActivity(), mPlaylistList, playlistId);
+ } else {
+ final long newId = MusicUtils.createPlaylist(getActivity(),
+ Capitalize.capitalize(playlistName));
+ MusicUtils.addToPlaylist(getActivity(), mPlaylistList, newId);
+ }
+ closeKeyboard();
+ getDialog().dismiss();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTextChangedListener() {
+ final String playlistName = mPlaylist.getText().toString();
+ mSaveButton = mPlaylistDialog.getButton(Dialog.BUTTON_POSITIVE);
+ if (mSaveButton == null) {
+ return;
+ }
+ if (playlistName.trim().length() == 0) {
+ mSaveButton.setEnabled(false);
+ } else {
+ mSaveButton.setEnabled(true);
+ if (MusicUtils.getIdForPlaylist(getActivity(), playlistName) >= 0) {
+ mSaveButton.setText(R.string.overwrite);
+ } else {
+ mSaveButton.setText(R.string.save);
+ }
+ }
+ }
+
+ private String makePlaylistName() {
+ final String template = getString(R.string.new_playlist_name_template);
+ int num = 1;
+ final String[] projection = new String[] {
+ MediaStore.Audio.Playlists.NAME
+ };
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final String selection = MediaStore.Audio.Playlists.NAME + " != ''";
+ Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, projection,
+ selection, null, MediaStore.Audio.Playlists.NAME);
+ if (cursor == null) {
+ return null;
+ }
+
+ String suggestedname;
+ suggestedname = String.format(template, num++);
+ boolean done = false;
+ while (!done) {
+ done = true;
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ final String playlistname = cursor.getString(0);
+ if (playlistname.compareToIgnoreCase(suggestedname) == 0) {
+ suggestedname = String.format(template, num++);
+ done = false;
+ }
+ cursor.moveToNext();
+ }
+ }
+ cursor.close();
+ cursor = null;
+ return suggestedname;
+ }
+}
diff --git a/src/com/cyngn/eleven/menu/DeleteDialog.java b/src/com/cyngn/eleven/menu/DeleteDialog.java
new file mode 100644
index 0000000..41c1422
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/DeleteDialog.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * Alert dialog used to delete tracks.
+ * <p>
+ * TODO: Remove albums from the recents list upon deletion.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class DeleteDialog extends DialogFragment {
+
+ public interface DeleteDialogCallback {
+ public void onDelete(long[] id);
+ }
+
+ /**
+ * The item(s) to delete
+ */
+ private long[] mItemList;
+
+ /**
+ * The image cache
+ */
+ private ImageFetcher mFetcher;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public DeleteDialog() {
+ }
+
+ /**
+ * @param title The title of the artist, album, or song to delete
+ * @param items The item(s) to delete
+ * @param key The key used to remove items from the cache.
+ * @return A new instance of the dialog
+ */
+ public static DeleteDialog newInstance(final String title, final long[] items, final String key) {
+ final DeleteDialog frag = new DeleteDialog();
+ final Bundle args = new Bundle();
+ args.putString(Config.NAME, title);
+ args.putLongArray("items", items);
+ args.putString("cachekey", key);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ final String delete = getString(R.string.context_menu_delete);
+ final Bundle arguments = getArguments();
+ // Get the image cache key
+ final String key = arguments.getString("cachekey");
+ // Get the track(s) to delete
+ mItemList = arguments.getLongArray("items");
+ // Get the dialog title
+ final String title = arguments.getString(Config.NAME);
+ final String dialogTitle = getString(R.string.delete_dialog_title, title);
+ // Initialize the image cache
+ mFetcher = ApolloUtils.getImageFetcher(getActivity());
+ // Build the dialog
+ return new AlertDialog.Builder(getActivity()).setTitle(dialogTitle)
+ .setMessage(R.string.cannot_be_undone)
+ .setPositiveButton(delete, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ // Remove the items from the image cache
+ mFetcher.removeFromCache(key);
+ // Delete the selected item(s)
+ MusicUtils.deleteTracks(getActivity(), mItemList);
+ if (getActivity() instanceof DeleteDialogCallback) {
+ ((DeleteDialogCallback)getActivity()).onDelete(mItemList);
+ }
+ dialog.dismiss();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).create();
+ }
+}
diff --git a/src/com/cyngn/eleven/menu/FragmentMenuItems.java b/src/com/cyngn/eleven/menu/FragmentMenuItems.java
new file mode 100644
index 0000000..37ad97c
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/FragmentMenuItems.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+/**
+ * Several of the context menu items used in Apollo are reused. This class helps
+ * keep things tidy.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FragmentMenuItems {
+
+ /* Removes a single album from the recents pages */
+ public static final int REMOVE_FROM_RECENT = 0;
+
+ /* Used to play the selected artist, album, song, playlist, or genre */
+ public static final int PLAY_SELECTION = 1;
+
+ /* Used to add to the qeueue */
+ public static final int ADD_TO_QUEUE = 2;
+
+ /* Used to add to a playlist */
+ public static final int ADD_TO_PLAYLIST = 3;
+
+ /* Used to add to the favorites cache */
+ public static final int ADD_TO_FAVORITES = 4;
+
+ /* Used to create a new playlist */
+ public static final int NEW_PLAYLIST = 5;
+
+ /* Used to rename a playlist */
+ public static final int RENAME_PLAYLIST = 6;
+
+ /* Used to add to a current playlist */
+ public static final int PLAYLIST_SELECTED = 7;
+
+ /* Used to show more content by an artist */
+ public static final int MORE_BY_ARTIST = 8;
+
+ /* Used to delete track(s) */
+ public static final int DELETE = 9;
+
+ /* Used to fetch an artist image */
+ public static final int FETCH_ARTIST_IMAGE = 10;
+
+ /* Used to fetch album art */
+ public static final int FETCH_ALBUM_ART = 11;
+
+ /* Used to set a track as a ringtone */
+ public static final int USE_AS_RINGTONE = 12;
+
+ /* Used to remove a track from the favorites cache */
+ public static final int REMOVE_FROM_FAVORITES = 13;
+
+ /* Used to remove a track from a playlist */
+ public static final int REMOVE_FROM_PLAYLIST = 14;
+
+ /* Used to remove a track from the queue */
+ public static final int REMOVE_FROM_QUEUE = 15;
+
+ /* Used to queue a track to be played next */
+ public static final int PLAY_NEXT = 16;
+
+}
diff --git a/src/com/cyngn/eleven/menu/PhotoSelectionDialog.java b/src/com/cyngn/eleven/menu/PhotoSelectionDialog.java
new file mode 100644
index 0000000..5b9245f
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/PhotoSelectionDialog.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Used when the user touches the image in the header in {@link ProfileActivity}
+ * . It provides an easy interface for them to choose a new image, use the old
+ * image, or search Google for one.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PhotoSelectionDialog extends DialogFragment {
+
+ private static final int NEW_PHOTO = 0;
+
+ private static final int OLD_PHOTO = 1;
+
+ private static final int GOOGLE_SEARCH = 2;
+
+ private static final int FETCH_IMAGE = 3;
+
+ private final ArrayList<String> mChoices = Lists.newArrayList();
+
+ private static ProfileType mProfileType;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PhotoSelectionDialog() {
+ }
+
+ /**
+ * @param title The dialog title.
+ * @param value The MIME type
+ * @return A new instance of the dialog.
+ */
+ public static PhotoSelectionDialog newInstance(final String title, final ProfileType type) {
+ final PhotoSelectionDialog frag = new PhotoSelectionDialog();
+ final Bundle args = new Bundle();
+ args.putString(Config.NAME, title);
+ frag.setArguments(args);
+ mProfileType = type;
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ final String title = getArguments().getString(Config.NAME);
+ switch (mProfileType) {
+ case ARTIST:
+ setArtistChoices();
+ break;
+ case ALBUM:
+ setAlbumChoices();
+ break;
+ case OTHER:
+ setOtherChoices();
+ break;
+ default:
+ break;
+ }
+ // Dialog item Adapter
+ final ProfileActivity activity = (ProfileActivity) getActivity();
+ final ListAdapter adapter = new ArrayAdapter<String>(activity,
+ android.R.layout.select_dialog_item, mChoices);
+ return new AlertDialog.Builder(activity).setTitle(title)
+ .setAdapter(adapter, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ switch (which) {
+ case NEW_PHOTO:
+ activity.selectNewPhoto();
+ break;
+ case OLD_PHOTO:
+ activity.selectOldPhoto();
+ break;
+ case FETCH_IMAGE:
+ activity.fetchAlbumArt();
+ break;
+ case GOOGLE_SEARCH:
+ activity.googleSearch();
+ break;
+ default:
+ break;
+ }
+ }
+ }).create();
+ }
+
+ /**
+ * Adds the choices for the artist profile image.
+ */
+ private void setArtistChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ if (ApolloUtils.isOnline(getActivity())) {
+ // Option to fetch the old artist image
+ mChoices.add(OLD_PHOTO, getString(R.string.context_menu_fetch_artist_image));
+ // Search Google for the artist name
+ mChoices.add(GOOGLE_SEARCH, getString(R.string.google_search));
+ }
+ }
+
+ /**
+ * Adds the choices for the album profile image.
+ */
+ private void setAlbumChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to fetch the old album image
+ mChoices.add(OLD_PHOTO, getString(R.string.old_photo));
+ if (ApolloUtils.isOnline(getActivity())) {
+ // Search Google for the album name
+ mChoices.add(GOOGLE_SEARCH, getString(R.string.google_search));
+ // Option to fetch the album image
+ mChoices.add(FETCH_IMAGE, getString(R.string.context_menu_fetch_album_art));
+ }
+ }
+
+ /**
+ * Adds the choices for the genre and playlist images.
+ */
+ private void setOtherChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to use the default image
+ mChoices.add(OLD_PHOTO, getString(R.string.use_default));
+ }
+
+ /**
+ * Easily detect the MIME type
+ */
+ public enum ProfileType {
+ ARTIST, ALBUM, OTHER
+ }
+}
diff --git a/src/com/cyngn/eleven/menu/RenamePlaylist.java b/src/com/cyngn/eleven/menu/RenamePlaylist.java
new file mode 100644
index 0000000..2393c29
--- /dev/null
+++ b/src/com/cyngn/eleven/menu/RenamePlaylist.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.menu;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.format.Capitalize;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * Alert dialog used to rename playlits.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RenamePlaylist extends BasePlaylistDialog {
+
+ private String mOriginalName;
+
+ private long mRenameId;
+
+ /**
+ * @param id The Id of the playlist to rename
+ * @return A new instance of this dialog.
+ */
+ public static RenamePlaylist getInstance(final Long id) {
+ final RenamePlaylist frag = new RenamePlaylist();
+ final Bundle args = new Bundle();
+ args.putLong("rename", id);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outcicle) {
+ outcicle.putString("defaultname", mPlaylist.getText().toString());
+ outcicle.putLong("rename", mRenameId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void initObjects(final Bundle savedInstanceState) {
+ mRenameId = savedInstanceState != null ? savedInstanceState.getLong("rename")
+ : getArguments().getLong("rename", -1);
+ mOriginalName = getPlaylistNameFromId(mRenameId);
+ mDefaultname = savedInstanceState != null ? savedInstanceState.getString("defaultname")
+ : mOriginalName;
+ if (mRenameId < 0 || mOriginalName == null || mDefaultname == null) {
+ getDialog().dismiss();
+ return;
+ }
+ final String promptformat = getString(R.string.create_playlist_prompt);
+ mPrompt = String.format(promptformat, mOriginalName, mDefaultname);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveClick() {
+ final String playlistName = mPlaylist.getText().toString();
+ if (playlistName != null && playlistName.length() > 0) {
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final ContentValues values = new ContentValues(1);
+ values.put(Audio.Playlists.NAME, Capitalize.capitalize(playlistName));
+ resolver.update(Audio.Playlists.EXTERNAL_CONTENT_URI, values,
+ MediaStore.Audio.Playlists._ID + "=?", new String[] {
+ String.valueOf(mRenameId)
+ });
+ closeKeyboard();
+ getDialog().dismiss();
+ }
+ }
+
+ @Override
+ public void onTextChangedListener() {
+ final String playlistName = mPlaylist.getText().toString();
+ mSaveButton = mPlaylistDialog.getButton(Dialog.BUTTON_POSITIVE);
+ if (mSaveButton == null) {
+ return;
+ }
+ if (playlistName.trim().length() == 0) {
+ mSaveButton.setEnabled(false);
+ } else {
+ mSaveButton.setEnabled(true);
+ if (MusicUtils.getIdForPlaylist(getActivity(), playlistName) >= 0) {
+ mSaveButton.setText(R.string.overwrite);
+ } else {
+ mSaveButton.setText(R.string.save);
+ }
+ }
+ }
+
+ /**
+ * @param id The Id of the playlist
+ * @return The name of the playlist
+ */
+ private String getPlaylistNameFromId(final long id) {
+ Cursor cursor = getActivity().getContentResolver().query(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
+ MediaStore.Audio.Playlists.NAME
+ }, MediaStore.Audio.Playlists._ID + "=?", new String[] {
+ String.valueOf(id)
+ }, MediaStore.Audio.Playlists.NAME);
+ String playlistName = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ playlistName = cursor.getString(0);
+ }
+ }
+ cursor.close();
+ cursor = null;
+ return playlistName;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/model/Album.java b/src/com/cyngn/eleven/model/Album.java
new file mode 100644
index 0000000..14c8031
--- /dev/null
+++ b/src/com/cyngn/eleven/model/Album.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.model;
+
+import android.text.TextUtils;
+
+/**
+ * A class that represents an album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Album {
+
+ /**
+ * The unique Id of the album
+ */
+ public long mAlbumId;
+
+ /**
+ * The name of the album
+ */
+ public String mAlbumName;
+
+ /**
+ * The album artist
+ */
+ public String mArtistName;
+
+ /**
+ * The number of songs in the album
+ */
+ public int mSongNumber;
+
+ /**
+ * The year the album was released
+ */
+ public String mYear;
+
+ /**
+ * Constructor of <code>Album</code>
+ *
+ * @param albumId The Id of the album
+ * @param albumName The name of the album
+ * @param artistName The album artist
+ * @param songNumber The number of songs in the album
+ * @param albumYear The year the album was released
+ */
+ public Album(final long albumId, final String albumName, final String artistName,
+ final int songNumber, final String albumYear) {
+ super();
+ mAlbumId = albumId;
+ mAlbumName = albumName;
+ mArtistName = artistName;
+ mSongNumber = songNumber;
+ mYear = albumYear;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) mAlbumId;
+ result = prime * result + (mAlbumName == null ? 0 : mAlbumName.hashCode());
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + mSongNumber;
+ result = prime * result + (mYear == null ? 0 : mYear.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Album other = (Album)obj;
+ if (mAlbumId != other.mAlbumId) {
+ return false;
+ }
+ if (!TextUtils.equals(mAlbumName, other.mAlbumName)) {
+ return false;
+ }
+ if (!TextUtils.equals(mArtistName, other.mArtistName)) {
+ return false;
+ }
+ if (mSongNumber != other.mSongNumber) {
+ return false;
+ }
+ if (!TextUtils.equals(mYear, other.mYear)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mAlbumName;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/model/Artist.java b/src/com/cyngn/eleven/model/Artist.java
new file mode 100644
index 0000000..c4db64e
--- /dev/null
+++ b/src/com/cyngn/eleven/model/Artist.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.model;
+
+import android.text.TextUtils;
+
+/**
+ * A class that represents an artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Artist {
+
+ /**
+ * The unique Id of the artist
+ */
+ public long mArtistId;
+
+ /**
+ * The artist name
+ */
+ public String mArtistName;
+
+ /**
+ * The number of albums for the artist
+ */
+ public int mAlbumNumber;
+
+ /**
+ * The number of songs for the artist
+ */
+ public int mSongNumber;
+
+ /**
+ * Constructor of <code>Artist</code>
+ *
+ * @param artistId The Id of the artist
+ * @param artistName The artist name
+ * @param songNumber The number of songs for the artist
+ * @param albumNumber The number of albums for the artist
+ */
+ public Artist(final long artistId, final String artistName, final int songNumber,
+ final int albumNumber) {
+ super();
+ mArtistId = artistId;
+ mArtistName = artistName;
+ mSongNumber = songNumber;
+ mAlbumNumber = albumNumber;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mAlbumNumber;
+ result = prime * result + (int) mArtistId;
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + mSongNumber;
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Artist other = (Artist)obj;
+ if (mAlbumNumber != other.mAlbumNumber) {
+ return false;
+ }
+ if (mArtistId != other.mArtistId) {
+ return false;
+ }
+ if (!TextUtils.equals(mArtistName, other.mArtistName)) {
+ return false;
+ }
+ if (mSongNumber != other.mSongNumber) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mArtistName;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/model/Genre.java b/src/com/cyngn/eleven/model/Genre.java
new file mode 100644
index 0000000..3a95f26
--- /dev/null
+++ b/src/com/cyngn/eleven/model/Genre.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.model;
+
+import android.text.TextUtils;
+
+/**
+ * A class that represents a genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Genre {
+
+ /**
+ * The unique Id of the genre
+ */
+ public long mGenreId;
+
+ /**
+ * The genre name
+ */
+ public String mGenreName;
+
+ /**
+ * Constructor of <code>Genre</code>
+ *
+ * @param genreId The Id of the genre
+ * @param genreName The genre name
+ */
+ public Genre(final long genreId, final String genreName) {
+ super();
+ mGenreId = genreId;
+ mGenreName = genreName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) mGenreId;
+ result = prime * result + (mGenreName == null ? 0 : mGenreName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Genre other = (Genre)obj;
+ if (mGenreId != other.mGenreId) {
+ return false;
+ }
+ return TextUtils.equals(mGenreName, other.mGenreName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mGenreName;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/model/Playlist.java b/src/com/cyngn/eleven/model/Playlist.java
new file mode 100644
index 0000000..bb40a7b
--- /dev/null
+++ b/src/com/cyngn/eleven/model/Playlist.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.model;
+
+import android.text.TextUtils;
+
+/**
+ * A class that represents a playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Playlist {
+
+ /**
+ * The unique Id of the playlist
+ */
+ public long mPlaylistId;
+
+ /**
+ * The playlist name
+ */
+ public String mPlaylistName;
+
+ /**
+ * Constructor of <code>Genre</code>
+ *
+ * @param playlistId The Id of the playlist
+ * @param playlistName The playlist name
+ */
+ public Playlist(final long playlistId, final String playlistName) {
+ super();
+ mPlaylistId = playlistId;
+ mPlaylistName = playlistName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) mPlaylistId;
+ result = prime * result + (mPlaylistName == null ? 0 : mPlaylistName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Playlist other = (Playlist)obj;
+ if (mPlaylistId != other.mPlaylistId) {
+ return false;
+ }
+ return TextUtils.equals(mPlaylistName, other.mPlaylistName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mPlaylistName;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/model/Song.java b/src/com/cyngn/eleven/model/Song.java
new file mode 100644
index 0000000..e5e916a
--- /dev/null
+++ b/src/com/cyngn/eleven/model/Song.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.model;
+
+import android.text.TextUtils;
+
+/**
+ * A class that represents a song.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Song {
+
+ /**
+ * The unique Id of the song
+ */
+ public long mSongId;
+
+ /**
+ * The song name
+ */
+ public String mSongName;
+
+ /**
+ * The song artist
+ */
+ public String mArtistName;
+
+ /**
+ * The song album
+ */
+ public String mAlbumName;
+
+ /**
+ * The song duration in seconds
+ */
+ public int mDuration;
+
+ /**
+ * Constructor of <code>Song</code>
+ *
+ * @param songId The Id of the song
+ * @param songName The name of the song
+ * @param artistName The song artist
+ * @param albumName The song album
+ * @param duration The duration of a song in seconds
+ */
+ public Song(final long songId, final String songName, final String artistName,
+ final String albumName, final int duration) {
+ mSongId = songId;
+ mSongName = songName;
+ mArtistName = artistName;
+ mAlbumName = albumName;
+ mDuration = duration;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mAlbumName == null ? 0 : mAlbumName.hashCode());
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + mDuration;
+ result = prime * result + (int) mSongId;
+ result = prime * result + (mSongName == null ? 0 : mSongName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Song other = (Song)obj;
+ if (mSongId != other.mSongId) {
+ return false;
+ }
+ if (!TextUtils.equals(mAlbumName, other.mAlbumName)) {
+ return false;
+ }
+ if (!TextUtils.equals(mArtistName, other.mArtistName)) {
+ return false;
+ }
+ if (mDuration != other.mDuration) {
+ return false;
+ }
+ if (!TextUtils.equals(mSongName, other.mSongName)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mSongName;
+ }
+}
diff --git a/src/com/cyngn/eleven/provider/FavoritesStore.java b/src/com/cyngn/eleven/provider/FavoritesStore.java
new file mode 100644
index 0000000..2220e28
--- /dev/null
+++ b/src/com/cyngn/eleven/provider/FavoritesStore.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * This class is used to to create the database used to make the Favorites
+ * playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoritesStore extends SQLiteOpenHelper {
+
+ /* Version constant to increment when the database should be rebuilt */
+ private static final int VERSION = 1;
+
+ /* Name of database file */
+ public static final String DATABASENAME = "favorites.db";
+
+ private static FavoritesStore sInstance = null;
+
+ /**
+ * Constructor of <code>FavoritesStore</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public FavoritesStore(final Context context) {
+ super(context, DATABASENAME, null, VERSION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + FavoriteColumns.NAME + " (" + FavoriteColumns.ID
+ + " LONG NOT NULL," + FavoriteColumns.SONGNAME + " TEXT NOT NULL,"
+ + FavoriteColumns.ALBUMNAME + " TEXT NOT NULL," + FavoriteColumns.ARTISTNAME
+ + " TEXT NOT NULL," + FavoriteColumns.PLAYCOUNT + " LONG NOT NULL);");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + FavoriteColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class
+ */
+ public static final synchronized FavoritesStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new FavoritesStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Used to store song Ids in our database
+ *
+ * @param songId The album's ID
+ * @param songName The song name
+ * @param albumName The album name
+ * @param artistName The artist name
+ */
+ public void addSongId(final Long songId, final String songName, final String albumName,
+ final String artistName) {
+ if (songId == null || songName == null || albumName == null || artistName == null) {
+ return;
+ }
+
+ final Long playCount = getPlayCount(songId);
+ final SQLiteDatabase database = getWritableDatabase();
+ final ContentValues values = new ContentValues(5);
+
+ database.beginTransaction();
+
+ values.put(FavoriteColumns.ID, songId);
+ values.put(FavoriteColumns.SONGNAME, songName);
+ values.put(FavoriteColumns.ALBUMNAME, albumName);
+ values.put(FavoriteColumns.ARTISTNAME, artistName);
+ values.put(FavoriteColumns.PLAYCOUNT, playCount != 0 ? playCount + 1 : 1);
+
+ database.delete(FavoriteColumns.NAME, FavoriteColumns.ID + " = ?", new String[] {
+ String.valueOf(songId)
+ });
+ database.insert(FavoriteColumns.NAME, null, values);
+ database.setTransactionSuccessful();
+ database.endTransaction();
+
+ }
+
+ /**
+ * Used to retrieve a single song Id from our database
+ *
+ * @param songId The song Id to reference
+ * @return The song Id
+ */
+ public Long getSongId(final Long songId) {
+ if (songId <= -1) {
+ return null;
+ }
+
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ FavoriteColumns.ID, FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ };
+ final String selection = FavoriteColumns.ID + "=?";
+ final String[] having = new String[] {
+ String.valueOf(songId)
+ };
+ Cursor cursor = database.query(FavoriteColumns.NAME, projection, selection, having, null,
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Long id = cursor.getLong(cursor.getColumnIndexOrThrow(FavoriteColumns.ID));
+ cursor.close();
+ cursor = null;
+ return id;
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ return null;
+ }
+
+ /**
+ * Used to retrieve the play count
+ *
+ * @param songId The song Id to reference
+ * @return The play count for a song
+ */
+ public Long getPlayCount(final Long songId) {
+ if (songId <= -1) {
+ return null;
+ }
+
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ FavoriteColumns.ID, FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ };
+ final String selection = FavoriteColumns.ID + "=?";
+ final String[] having = new String[] {
+ String.valueOf(songId)
+ };
+ Cursor cursor = database.query(FavoriteColumns.NAME, projection, selection, having, null,
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Long playCount = cursor.getLong(cursor
+ .getColumnIndexOrThrow(FavoriteColumns.PLAYCOUNT));
+ cursor.close();
+ cursor = null;
+ return playCount;
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+
+ return (long)0;
+ }
+
+ /**
+ * Clear the cache.
+ *
+ * @param context The {@link Context} to use.
+ */
+ public static void deleteDatabase(final Context context) {
+ context.deleteDatabase(DATABASENAME);
+ }
+
+ /**
+ * Toggle the current song as favorite
+ */
+ public void toggleSong(final Long songId, final String songName, final String albumName,
+ final String artistName) {
+ if (getSongId(songId) == null) {
+ addSongId(songId, songName, albumName, artistName);
+ } else {
+ removeItem(songId);
+ }
+
+ }
+
+ /**
+ * @param item The song Id to remove
+ */
+ public void removeItem(final Long songId) {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(FavoriteColumns.NAME, FavoriteColumns.ID + " = ?", new String[] {
+ String.valueOf(songId)
+ });
+
+ }
+
+ public interface FavoriteColumns {
+
+ /* Table name */
+ public static final String NAME = "favorites";
+
+ /* Song IDs column */
+ public static final String ID = "songid";
+
+ /* Song name column */
+ public static final String SONGNAME = "songname";
+
+ /* Album name column */
+ public static final String ALBUMNAME = "albumname";
+
+ /* Artist name column */
+ public static final String ARTISTNAME = "artistname";
+
+ /* Play count column */
+ public static final String PLAYCOUNT = "playcount";
+ }
+
+}
diff --git a/src/com/cyngn/eleven/provider/RecentStore.java b/src/com/cyngn/eleven/provider/RecentStore.java
new file mode 100644
index 0000000..0663e2b
--- /dev/null
+++ b/src/com/cyngn/eleven/provider/RecentStore.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+
+/**
+ * The {@link RecentlyListenedFragment} is used to display a a grid or list of
+ * recently listened to albums. In order to populate the this grid or list with
+ * the correct data, we keep a cache of the album ID, name, and time it was
+ * played to be retrieved later.
+ * <p>
+ * In {@link ProfileActivity}, when viewing the profile for an artist, the first
+ * image the carousel header is the last album the user listened to for that
+ * particular artist. That album is retrieved using
+ * {@link #getAlbumName(String)}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentStore extends SQLiteOpenHelper {
+
+ /* Version constant to increment when the database should be rebuilt */
+ private static final int VERSION = 1;
+
+ /* Name of database file */
+ public static final String DATABASENAME = "albumhistory.db";
+
+ private static RecentStore sInstance = null;
+
+ /**
+ * Constructor of <code>RecentStore</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public RecentStore(final Context context) {
+ super(context, DATABASENAME, null, VERSION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + RecentStoreColumns.NAME + " ("
+ + RecentStoreColumns.ID + " LONG NOT NULL," + RecentStoreColumns.ALBUMNAME
+ + " TEXT NOT NULL," + RecentStoreColumns.ARTISTNAME + " TEXT NOT NULL,"
+ + RecentStoreColumns.ALBUMSONGCOUNT + " TEXT NOT NULL,"
+ + RecentStoreColumns.ALBUMYEAR + " TEXT," + RecentStoreColumns.TIMEPLAYED
+ + " LONG NOT NULL);");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static final synchronized RecentStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new RecentStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Used to store artist IDs in the database.
+ *
+ * @param albumIDdThe album's ID.
+ * @param albumName The album name.
+ * @param artistName The artist album name.
+ * @param songCount The number of tracks for the album.
+ * @param albumYear The year the album was released.
+ */
+ public void addAlbumId(final Long albumId, final String albumName, final String artistName,
+ final String songCount, final String albumYear) {
+ if (albumId == null || albumName == null || artistName == null || songCount == null) {
+ return;
+ }
+
+ final SQLiteDatabase database = getWritableDatabase();
+ final ContentValues values = new ContentValues(6);
+
+ database.beginTransaction();
+
+ values.put(RecentStoreColumns.ID, albumId);
+ values.put(RecentStoreColumns.ALBUMNAME, albumName);
+ values.put(RecentStoreColumns.ARTISTNAME, artistName);
+ values.put(RecentStoreColumns.ALBUMSONGCOUNT, songCount);
+ values.put(RecentStoreColumns.ALBUMYEAR, albumYear);
+ values.put(RecentStoreColumns.TIMEPLAYED, System.currentTimeMillis());
+
+ database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[] {
+ String.valueOf(albumId)
+ });
+ database.insert(RecentStoreColumns.NAME, null, values);
+ database.setTransactionSuccessful();
+ database.endTransaction();
+
+ }
+
+ /**
+ * Used to retrieve the most recently listened album for an artist.
+ *
+ * @param key The key to reference.
+ * @return The most recently listened album for an artist.
+ */
+ public String getAlbumName(final String key) {
+ if (TextUtils.isEmpty(key)) {
+ return null;
+ }
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ RecentStoreColumns.ID, RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.TIMEPLAYED
+ };
+ final String selection = RecentStoreColumns.ARTISTNAME + "=?";
+ final String[] having = new String[] {
+ key
+ };
+ Cursor cursor = database.query(RecentStoreColumns.NAME, projection, selection, having,
+ null, null, RecentStoreColumns.TIMEPLAYED + " DESC", null);
+ if (cursor != null && cursor.moveToFirst()) {
+ cursor.moveToFirst();
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+ cursor.close();
+ cursor = null;
+ return album;
+ }
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ cursor = null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void deleteDatabase() {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(RecentStoreColumns.NAME, null, null);
+ }
+
+ /**
+ * @param item The album Id to remove.
+ */
+ public void removeItem(final long albumId) {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[] {
+ String.valueOf(albumId)
+ });
+
+ }
+
+ public interface RecentStoreColumns {
+
+ /* Table name */
+ public static final String NAME = "albumhistory";
+
+ /* Album IDs column */
+ public static final String ID = "albumid";
+
+ /* Album name column */
+ public static final String ALBUMNAME = "itemname";
+
+ /* Artist name column */
+ public static final String ARTISTNAME = "artistname";
+
+ /* Album song count column */
+ public static final String ALBUMSONGCOUNT = "albumsongcount";
+
+ /* Album year column. It's okay for this to be null */
+ public static final String ALBUMYEAR = "albumyear";
+
+ /* Time played column */
+ public static final String TIMEPLAYED = "timeplayed";
+ }
+}
diff --git a/src/com/cyngn/eleven/recycler/RecycleHolder.java b/src/com/cyngn/eleven/recycler/RecycleHolder.java
new file mode 100644
index 0000000..c89c129
--- /dev/null
+++ b/src/com/cyngn/eleven/recycler/RecycleHolder.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.recycler;
+
+import android.view.View;
+import android.widget.AbsListView.RecyclerListener;
+
+import com.cyngn.eleven.ui.MusicHolder;
+
+/**
+ * A @ {@link RecyclerListener} for {@link MusicHolder}'s views.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecycleHolder implements RecyclerListener {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMovedToScrapHeap(final View view) {
+ MusicHolder holder = (MusicHolder)view.getTag();
+ if (holder == null) {
+ holder = new MusicHolder(view);
+ view.setTag(holder);
+ }
+
+ // Release mBackground's reference
+ if (holder.mBackground.get() != null) {
+ holder.mBackground.get().setImageDrawable(null);
+ holder.mBackground.get().setImageBitmap(null);
+ }
+
+ // Release mImage's reference
+ if (holder.mImage.get() != null) {
+ holder.mImage.get().setImageDrawable(null);
+ holder.mImage.get().setImageBitmap(null);
+ }
+
+ // Release mLineOne's reference
+ if (holder.mLineOne.get() != null) {
+ holder.mLineOne.get().setText(null);
+ }
+
+ // Release mLineTwo's reference
+ if (holder.mLineTwo.get() != null) {
+ holder.mLineTwo.get().setText(null);
+ }
+
+ // Release mLineThree's reference
+ if (holder.mLineThree.get() != null) {
+ holder.mLineThree.get().setText(null);
+ }
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/MusicHolder.java b/src/com/cyngn/eleven/ui/MusicHolder.java
new file mode 100644
index 0000000..1d30e15
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/MusicHolder.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.appwidgets.RecentWidgetService;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Used to efficiently cache and recyle the {@link View}s used in the artist,
+ * album, song, playlist, and genre adapters.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class MusicHolder {
+
+ /**
+ * This is the overlay ontop of the background artist, playlist, or genre
+ * image
+ */
+ public WeakReference<RelativeLayout> mOverlay;
+
+ /**
+ * This is the background artist, playlist, or genre image
+ */
+ public WeakReference<ImageView> mBackground;
+
+ /**
+ * This is the artist or album image
+ */
+ public WeakReference<ImageView> mImage;
+
+ /**
+ * This is the first line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineOne;
+
+ /**
+ * This is displayed on the right side of the first line in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineOneRight;
+
+ /**
+ * This is the second line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineTwo;
+
+ /**
+ * This is the third line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineThree;
+
+ /**
+ * Constructor of <code>ViewHolder</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public MusicHolder(final View view) {
+ super();
+ // Initialize mOverlay
+ mOverlay = new WeakReference<RelativeLayout>(
+ (RelativeLayout)view.findViewById(R.id.image_background));
+
+ // Initialize mBackground
+ mBackground = new WeakReference<ImageView>(
+ (ImageView)view.findViewById(R.id.list_item_background));
+
+ // Initialize mImage
+ mImage = new WeakReference<ImageView>((ImageView)view.findViewById(R.id.image));
+
+ // Initialize mLineOne
+ mLineOne = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_one));
+
+ // Initialize mLineOneRight
+ mLineOneRight = new WeakReference<TextView>(
+ (TextView)view.findViewById(R.id.line_one_right));
+
+ // Initialize mLineTwo
+ mLineTwo = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_two));
+
+ // Initialize mLineThree
+ mLineThree = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_three));
+ }
+
+ /**
+ * @param view The {@link View} used to initialize content
+ */
+ public final static class DataHolder {
+
+ /**
+ * This is the ID of the item being loaded in the adapter
+ */
+ public long mItemId;
+
+ /**
+ * This is the first line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineOne;
+
+ /**
+ * This is displayed on the right side of the first line in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineOneRight;
+
+ /**
+ * This is the second line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineTwo;
+
+ /**
+ * This is the third line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineThree;
+
+ /**
+ * This is the album art bitmap used in {@link RecentWidgetService}.
+ */
+ public Bitmap mImage;
+
+ /**
+ * Constructor of <code>DataHolder</code>
+ */
+ public DataHolder() {
+ super();
+ }
+
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/activities/AudioPlayerActivity.java b/src/com/cyngn/eleven/ui/activities/AudioPlayerActivity.java
new file mode 100644
index 0000000..c3b142d
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/AudioPlayerActivity.java
@@ -0,0 +1,984 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import static com.cyngn.eleven.utils.MusicUtils.mService;
+
+import android.animation.ObjectAnimator;
+import android.app.ActionBar;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.audiofx.AudioEffect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.MediaStore.Audio.Playlists;
+import android.provider.MediaStore.Audio.Albums;
+import android.provider.MediaStore.Audio.Artists;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.cyngn.eleven.IElevenService;
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.PagerAdapter;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.ui.fragments.QueueFragment;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.PlayPauseButton;
+import com.cyngn.eleven.widgets.RepeatButton;
+import com.cyngn.eleven.widgets.RepeatingImageButton;
+import com.cyngn.eleven.widgets.ShuffleButton;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Apollo's "now playing" interface.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AudioPlayerActivity extends FragmentActivity implements ServiceConnection,
+ OnSeekBarChangeListener, DeleteDialog.DeleteDialogCallback {
+
+ // Message to refresh the time
+ private static final int REFRESH_TIME = 1;
+
+ // The service token
+ private ServiceToken mToken;
+
+ // Play and pause button
+ private PlayPauseButton mPlayPauseButton;
+
+ // Repeat button
+ private RepeatButton mRepeatButton;
+
+ // Shuffle button
+ private ShuffleButton mShuffleButton;
+
+ // Previous button
+ private RepeatingImageButton mPreviousButton;
+
+ // Next button
+ private RepeatingImageButton mNextButton;
+
+ // Track name
+ private TextView mTrackName;
+
+ // Artist name
+ private TextView mArtistName;
+
+ // Album art
+ private ImageView mAlbumArt;
+
+ // Tiny artwork
+ private ImageView mAlbumArtSmall;
+
+ // Current time
+ private TextView mCurrentTime;
+
+ // Total time
+ private TextView mTotalTime;
+
+ // Queue switch
+ private ImageView mQueueSwitch;
+
+ // Progess
+ private SeekBar mProgress;
+
+ // Broadcast receiver
+ private PlaybackStatus mPlaybackStatus;
+
+ // Handler used to update the current time
+ private TimeHandler mTimeHandler;
+
+ // View pager
+ private ViewPager mViewPager;
+
+ // Pager adpater
+ private PagerAdapter mPagerAdapter;
+
+ // ViewPager container
+ private FrameLayout mPageContainer;
+
+ // Header
+ private LinearLayout mAudioPlayerHeader;
+
+ // Image cache
+ private ImageFetcher mImageFetcher;
+
+ // Theme resources
+ private ThemeUtils mResources;
+
+ private long mPosOverride = -1;
+
+ private long mStartSeekPos = 0;
+
+ private long mLastSeekEventTime;
+
+ private long mLastShortSeekEventTime;
+
+ private boolean mIsPaused = false;
+
+ private boolean mFromTouch = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Initialize the image fetcher/cache
+ mImageFetcher = ApolloUtils.getImageFetcher(this);
+
+ // Initialize the handler used to update the current time
+ mTimeHandler = new TimeHandler(this);
+
+ // Initialize the broadcast receiver
+ mPlaybackStatus = new PlaybackStatus(this);
+
+ // Theme the action bar
+ final ActionBar actionBar = getActionBar();
+ mResources.themeActionBar(actionBar, getString(R.string.app_name));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ // Set the layout
+ setContentView(R.layout.activity_player_base);
+
+ // Cache all the items
+ initPlaybackControls();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onNewIntent(Intent intent) {
+ setIntent(intent);
+ startPlayback();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IElevenService.Stub.asInterface(service);
+ // Check whether we were asked to start any playback
+ startPlayback();
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateNowPlayingInfo();
+ // Update the favorites icon
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onProgressChanged(final SeekBar bar, final int progress, final boolean fromuser) {
+ if (!fromuser || mService == null) {
+ return;
+ }
+ final long now = SystemClock.elapsedRealtime();
+ if (now - mLastSeekEventTime > 250) {
+ mLastSeekEventTime = now;
+ mLastShortSeekEventTime = now;
+ mPosOverride = MusicUtils.duration() * progress / 1000;
+ MusicUtils.seek(mPosOverride);
+ if (!mFromTouch) {
+ // refreshCurrentTime();
+ mPosOverride = -1;
+ }
+ } else if (now - mLastShortSeekEventTime > 5) {
+ mLastShortSeekEventTime = now;
+ mPosOverride = MusicUtils.duration() * progress / 1000;
+ refreshCurrentTimeText(mPosOverride);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onStartTrackingTouch(final SeekBar bar) {
+ mLastSeekEventTime = 0;
+ mFromTouch = true;
+ mCurrentTime.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onStopTrackingTouch(final SeekBar bar) {
+ if (mPosOverride != -1) {
+ MusicUtils.seek(mPosOverride);
+ }
+ mPosOverride = -1;
+ mFromTouch = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ // Hide the EQ option if it can't be opened
+ final Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
+ if (getPackageManager().resolveActivity(intent, 0) == null) {
+ final MenuItem effects = menu.findItem(R.id.menu_audio_player_equalizer);
+ effects.setVisible(false);
+ }
+ mResources.setFavoriteIcon(menu);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getMenuInflater().inflate(R.menu.search, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ final SearchView searchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ searchView.setSearchableInfo(searchableInfo);
+ // Perform the search
+ searchView.setOnQueryTextListener(new OnQueryTextListener() {
+
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ // Open the search activity
+ NavUtils.openSearch(AudioPlayerActivity.this, query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ // Nothing to do
+ return false;
+ }
+ });
+
+ // Favorite action
+ getMenuInflater().inflate(R.menu.favorite, menu);
+ // Shuffle all
+ getMenuInflater().inflate(R.menu.shuffle, menu);
+ // Share, ringtone, and equalizer
+ getMenuInflater().inflate(R.menu.audio_player, menu);
+ // Settings
+ getMenuInflater().inflate(R.menu.activity_base, menu);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // Go back to the home activity
+ NavUtils.goHome(this);
+ return true;
+ case R.id.menu_shuffle:
+ // Shuffle all the songs
+ MusicUtils.shuffleAll(this);
+ // Refresh the queue
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ return true;
+ case R.id.menu_favorite:
+ // Toggle the current track as a favorite and update the menu
+ // item
+ MusicUtils.toggleFavorite();
+ invalidateOptionsMenu();
+ return true;
+ case R.id.menu_audio_player_ringtone:
+ // Set the current track as a ringtone
+ MusicUtils.setRingtone(this, MusicUtils.getCurrentAudioId());
+ return true;
+ case R.id.menu_audio_player_share:
+ // Share the current meta data
+ shareCurrentTrack();
+ return true;
+ case R.id.menu_audio_player_equalizer:
+ // Sound effects
+ NavUtils.openEffectsPanel(this);
+ return true;
+ case R.id.menu_settings:
+ // Settings
+ NavUtils.openSettings(this);
+ return true;
+ case R.id.menu_audio_player_delete:
+ // Delete current song
+ DeleteDialog.newInstance(MusicUtils.getTrackName(), new long[] {
+ MusicUtils.getCurrentAudioId()
+ }, null).show(getSupportFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onDelete(long[] ids) {
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ if (MusicUtils.getQueue().length == 0) {
+ NavUtils.goHome(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ NavUtils.goHome(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateNowPlayingInfo();
+ // Refresh the queue
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final IntentFilter filter = new IntentFilter();
+ // Play and pause changes
+ filter.addAction(MusicPlaybackService.PLAYSTATE_CHANGED);
+ // Shuffle and repeat changes
+ filter.addAction(MusicPlaybackService.SHUFFLEMODE_CHANGED);
+ filter.addAction(MusicPlaybackService.REPEATMODE_CHANGED);
+ // Track changes
+ filter.addAction(MusicPlaybackService.META_CHANGED);
+ // Update a list, probably the playlist fragment's
+ filter.addAction(MusicPlaybackService.REFRESH);
+ registerReceiver(mPlaybackStatus, filter);
+ // Refresh the current time
+ final long next = refreshCurrentTime();
+ queueNextRefresh(next);
+ MusicUtils.notifyForegroundStateChanged(this, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ MusicUtils.notifyForegroundStateChanged(this, false);
+ mImageFetcher.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mIsPaused = false;
+ mTimeHandler.removeMessages(REFRESH_TIME);
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+
+ // Unregister the receiver
+ try {
+ unregisterReceiver(mPlaybackStatus);
+ } catch (final Throwable e) {
+ //$FALL-THROUGH$
+ }
+ }
+
+ /**
+ * Initializes the items in the now playing screen
+ */
+ @SuppressWarnings("deprecation")
+ private void initPlaybackControls() {
+ // ViewPager container
+ mPageContainer = (FrameLayout)findViewById(R.id.audio_player_pager_container);
+ // Theme the pager container background
+ mPageContainer
+ .setBackgroundDrawable(mResources.getDrawable("audio_player_pager_container"));
+
+ // Now playing header
+ mAudioPlayerHeader = (LinearLayout)findViewById(R.id.audio_player_header);
+ // Opens the currently playing album profile
+ mAudioPlayerHeader.setOnClickListener(mOpenAlbumProfile);
+
+ // Used to hide the artwork and show the queue
+ final FrameLayout mSwitch = (FrameLayout)findViewById(R.id.audio_player_switch);
+ mSwitch.setOnClickListener(mToggleHiddenPanel);
+
+ // Initialize the pager adapter
+ mPagerAdapter = new PagerAdapter(this);
+ // Queue
+ mPagerAdapter.add(QueueFragment.class, null);
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)findViewById(R.id.audio_player_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen pager loading limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Play and pause button
+ mPlayPauseButton = (PlayPauseButton)findViewById(R.id.action_button_play);
+ // Shuffle button
+ mShuffleButton = (ShuffleButton)findViewById(R.id.action_button_shuffle);
+ // Repeat button
+ mRepeatButton = (RepeatButton)findViewById(R.id.action_button_repeat);
+ // Previous button
+ mPreviousButton = (RepeatingImageButton)findViewById(R.id.action_button_previous);
+ // Next button
+ mNextButton = (RepeatingImageButton)findViewById(R.id.action_button_next);
+ // Track name
+ mTrackName = (TextView)findViewById(R.id.audio_player_track_name);
+ // Artist name
+ mArtistName = (TextView)findViewById(R.id.audio_player_artist_name);
+ // Album art
+ mAlbumArt = (ImageView)findViewById(R.id.audio_player_album_art);
+ // Small album art
+ mAlbumArtSmall = (ImageView)findViewById(R.id.audio_player_switch_album_art);
+ // Current time
+ mCurrentTime = (TextView)findViewById(R.id.audio_player_current_time);
+ // Total time
+ mTotalTime = (TextView)findViewById(R.id.audio_player_total_time);
+ // Used to show and hide the queue fragment
+ mQueueSwitch = (ImageView)findViewById(R.id.audio_player_switch_queue);
+ // Theme the queue switch icon
+ mQueueSwitch.setImageDrawable(mResources.getDrawable("btn_switch_queue"));
+ // Progress
+ mProgress = (SeekBar)findViewById(android.R.id.progress);
+
+ // Set the repeat listner for the previous button
+ mPreviousButton.setRepeatListener(mRewindListener);
+ // Set the repeat listner for the next button
+ mNextButton.setRepeatListener(mFastForwardListener);
+ // Update the progress
+ mProgress.setOnSeekBarChangeListener(this);
+ }
+
+ /**
+ * Sets the track name, album name, and album art.
+ */
+ private void updateNowPlayingInfo() {
+ // Set the track name
+ mTrackName.setText(MusicUtils.getTrackName());
+ // Set the artist name
+ mArtistName.setText(MusicUtils.getArtistName());
+ // Set the total time
+ mTotalTime.setText(MusicUtils.makeTimeString(this, MusicUtils.duration() / 1000));
+ // Set the album art
+ mImageFetcher.loadCurrentArtwork(mAlbumArt);
+ // Set the small artwork
+ mImageFetcher.loadCurrentArtwork(mAlbumArtSmall);
+ // Update the current time
+ queueNextRefresh(1);
+
+ }
+
+ private long parseIdFromIntent(Intent intent, String longKey,
+ String stringKey, long defaultId) {
+ long id = intent.getLongExtra(longKey, -1);
+ if (id < 0) {
+ String idString = intent.getStringExtra(stringKey);
+ if (idString != null) {
+ try {
+ id = Long.parseLong(idString);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ }
+ }
+ return id;
+ }
+
+ /**
+ * Checks whether the passed intent contains a playback request,
+ * and starts playback if that's the case
+ */
+ private void startPlayback() {
+ Intent intent = getIntent();
+
+ if (intent == null || mService == null) {
+ return;
+ }
+
+ Uri uri = intent.getData();
+ String mimeType = intent.getType();
+ boolean handled = false;
+
+ if (uri != null && uri.toString().length() > 0) {
+ MusicUtils.playFile(this, uri);
+ handled = true;
+ } else if (Playlists.CONTENT_TYPE.equals(mimeType)) {
+ long id = parseIdFromIntent(intent, "playlistId", "playlist", -1);
+ if (id >= 0) {
+ MusicUtils.playPlaylist(this, id);
+ handled = true;
+ }
+ } else if (Albums.CONTENT_TYPE.equals(mimeType)) {
+ long id = parseIdFromIntent(intent, "albumId", "album", -1);
+ if (id >= 0) {
+ int position = intent.getIntExtra("position", 0);
+ MusicUtils.playAlbum(this, id, position);
+ handled = true;
+ }
+ } else if (Artists.CONTENT_TYPE.equals(mimeType)) {
+ long id = parseIdFromIntent(intent, "artistId", "artist", -1);
+ if (id >= 0) {
+ int position = intent.getIntExtra("position", 0);
+ MusicUtils.playArtist(this, id, position);
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ // Make sure to process intent only once
+ setIntent(new Intent());
+ // Refresh the queue
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ }
+ }
+
+ /**
+ * Sets the correct drawable states for the playback controls.
+ */
+ private void updatePlaybackControls() {
+ // Set the play and pause image
+ mPlayPauseButton.updateState();
+ // Set the shuffle image
+ mShuffleButton.updateShuffleState();
+ // Set the repeat image
+ mRepeatButton.updateRepeatState();
+ }
+
+ /**
+ * @param delay When to update
+ */
+ private void queueNextRefresh(final long delay) {
+ if (!mIsPaused) {
+ final Message message = mTimeHandler.obtainMessage(REFRESH_TIME);
+ mTimeHandler.removeMessages(REFRESH_TIME);
+ mTimeHandler.sendMessageDelayed(message, delay);
+ }
+ }
+
+ /**
+ * Used to scan backwards in time through the curren track
+ *
+ * @param repcnt The repeat count
+ * @param delta The long press duration
+ */
+ private void scanBackward(final int repcnt, long delta) {
+ if (mService == null) {
+ return;
+ }
+ if (repcnt == 0) {
+ mStartSeekPos = MusicUtils.position();
+ mLastSeekEventTime = 0;
+ } else {
+ if (delta < 5000) {
+ // seek at 10x speed for the first 5 seconds
+ delta = delta * 10;
+ } else {
+ // seek at 40x after that
+ delta = 50000 + (delta - 5000) * 40;
+ }
+ long newpos = mStartSeekPos - delta;
+ if (newpos < 0) {
+ // move to previous track
+ MusicUtils.previous(this);
+ final long duration = MusicUtils.duration();
+ mStartSeekPos += duration;
+ newpos += duration;
+ }
+ if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
+ MusicUtils.seek(newpos);
+ mLastSeekEventTime = delta;
+ }
+ if (repcnt >= 0) {
+ mPosOverride = newpos;
+ } else {
+ mPosOverride = -1;
+ }
+ refreshCurrentTime();
+ }
+ }
+
+ /**
+ * Used to scan forwards in time through the curren track
+ *
+ * @param repcnt The repeat count
+ * @param delta The long press duration
+ */
+ private void scanForward(final int repcnt, long delta) {
+ if (mService == null) {
+ return;
+ }
+ if (repcnt == 0) {
+ mStartSeekPos = MusicUtils.position();
+ mLastSeekEventTime = 0;
+ } else {
+ if (delta < 5000) {
+ // seek at 10x speed for the first 5 seconds
+ delta = delta * 10;
+ } else {
+ // seek at 40x after that
+ delta = 50000 + (delta - 5000) * 40;
+ }
+ long newpos = mStartSeekPos + delta;
+ final long duration = MusicUtils.duration();
+ if (newpos >= duration) {
+ // move to next track
+ MusicUtils.next();
+ mStartSeekPos -= duration; // is OK to go negative
+ newpos -= duration;
+ }
+ if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
+ MusicUtils.seek(newpos);
+ mLastSeekEventTime = delta;
+ }
+ if (repcnt >= 0) {
+ mPosOverride = newpos;
+ } else {
+ mPosOverride = -1;
+ }
+ refreshCurrentTime();
+ }
+ }
+
+ private void refreshCurrentTimeText(final long pos) {
+ mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
+ }
+
+ /* Used to update the current time string */
+ private long refreshCurrentTime() {
+ if (mService == null) {
+ return 500;
+ }
+ try {
+ final long pos = mPosOverride < 0 ? MusicUtils.position() : mPosOverride;
+ if (pos >= 0 && MusicUtils.duration() > 0) {
+ refreshCurrentTimeText(pos);
+ final int progress = (int)(1000 * pos / MusicUtils.duration());
+ mProgress.setProgress(progress);
+
+ if (mFromTouch) {
+ return 500;
+ } else if (MusicUtils.isPlaying()) {
+ mCurrentTime.setVisibility(View.VISIBLE);
+ } else {
+ // blink the counter
+ final int vis = mCurrentTime.getVisibility();
+ mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE
+ : View.INVISIBLE);
+ return 500;
+ }
+ } else {
+ mCurrentTime.setText("--:--");
+ mProgress.setProgress(1000);
+ }
+ // calculate the number of milliseconds until the next full second,
+ // so
+ // the counter can be updated at just the right time
+ final long remaining = 1000 - pos % 1000;
+ // approximate how often we would need to refresh the slider to
+ // move it smoothly
+ int width = mProgress.getWidth();
+ if (width == 0) {
+ width = 320;
+ }
+ final long smoothrefreshtime = MusicUtils.duration() / width;
+ if (smoothrefreshtime > remaining) {
+ return remaining;
+ }
+ if (smoothrefreshtime < 20) {
+ return 20;
+ }
+ return smoothrefreshtime;
+ } catch (final Exception ignored) {
+
+ }
+ return 500;
+ }
+
+ /**
+ * @param v The view to animate
+ * @param alpha The alpha to apply
+ */
+ private void fade(final View v, final float alpha) {
+ final ObjectAnimator fade = ObjectAnimator.ofFloat(v, "alpha", alpha);
+ fade.setInterpolator(AnimationUtils.loadInterpolator(this,
+ android.R.anim.accelerate_decelerate_interpolator));
+ fade.setDuration(400);
+ fade.start();
+ }
+
+ /**
+ * Called to show the album art and hide the queue
+ */
+ private void showAlbumArt() {
+ mPageContainer.setVisibility(View.INVISIBLE);
+ mAlbumArtSmall.setVisibility(View.GONE);
+ mQueueSwitch.setVisibility(View.VISIBLE);
+ // Fade out the pager container
+ fade(mPageContainer, 0f);
+ // Fade in the album art
+ fade(mAlbumArt, 1f);
+ }
+
+ /**
+ * Called to hide the album art and show the queue
+ */
+ public void hideAlbumArt() {
+ mPageContainer.setVisibility(View.VISIBLE);
+ mQueueSwitch.setVisibility(View.GONE);
+ mAlbumArtSmall.setVisibility(View.VISIBLE);
+ // Fade out the artwork
+ fade(mAlbumArt, 0f);
+ // Fade in the pager container
+ fade(mPageContainer, 1f);
+ }
+
+ /**
+ * /** Used to shared what the user is currently listening to
+ */
+ private void shareCurrentTrack() {
+ if (MusicUtils.getTrackName() == null || MusicUtils.getArtistName() == null) {
+ return;
+ }
+ final Intent shareIntent = new Intent();
+ final String shareMessage = getString(R.string.now_listening_to,
+ MusicUtils.getTrackName(), MusicUtils.getArtistName());
+
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage);
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.share_track_using)));
+ }
+
+ /**
+ * Used to scan backwards through the track
+ */
+ private final RepeatingImageButton.RepeatListener mRewindListener = new RepeatingImageButton.RepeatListener() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRepeat(final View v, final long howlong, final int repcnt) {
+ scanBackward(repcnt, howlong);
+ }
+ };
+
+ /**
+ * Used to scan ahead through the track
+ */
+ private final RepeatingImageButton.RepeatListener mFastForwardListener = new RepeatingImageButton.RepeatListener() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRepeat(final View v, final long howlong, final int repcnt) {
+ scanForward(repcnt, howlong);
+ }
+ };
+
+ /**
+ * Switches from the large album art screen to show the queue and lyric
+ * fragments, then back again
+ */
+ private final OnClickListener mToggleHiddenPanel = new OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ if (mPageContainer.getVisibility() == View.VISIBLE) {
+ // Open the current album profile
+ mAudioPlayerHeader.setOnClickListener(mOpenAlbumProfile);
+ // Show the artwork, hide the queue
+ showAlbumArt();
+ } else {
+ // Scroll to the current track
+ mAudioPlayerHeader.setOnClickListener(mScrollToCurrentSong);
+ // Show the queue, hide the artwork
+ hideAlbumArt();
+ }
+ }
+ };
+
+ /**
+ * Opens to the current album profile
+ */
+ private final OnClickListener mOpenAlbumProfile = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ NavUtils.openAlbumProfile(AudioPlayerActivity.this, MusicUtils.getAlbumName(),
+ MusicUtils.getArtistName(), MusicUtils.getCurrentAlbumId());
+ }
+ };
+
+ /**
+ * Scrolls the queue to the currently playing song
+ */
+ private final OnClickListener mScrollToCurrentSong = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ ((QueueFragment)mPagerAdapter.getFragment(0)).scrollToCurrentSong();
+ }
+ };
+
+ /**
+ * Used to update the current time string
+ */
+ private static final class TimeHandler extends Handler {
+
+ private final WeakReference<AudioPlayerActivity> mAudioPlayer;
+
+ /**
+ * Constructor of <code>TimeHandler</code>
+ */
+ public TimeHandler(final AudioPlayerActivity player) {
+ mAudioPlayer = new WeakReference<AudioPlayerActivity>(player);
+ }
+
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case REFRESH_TIME:
+ final long next = mAudioPlayer.get().refreshCurrentTime();
+ mAudioPlayer.get().queueNextRefresh(next);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ /**
+ * Used to monitor the state of playback
+ */
+ private static final class PlaybackStatus extends BroadcastReceiver {
+
+ private final WeakReference<AudioPlayerActivity> mReference;
+
+ /**
+ * Constructor of <code>PlaybackStatus</code>
+ */
+ public PlaybackStatus(final AudioPlayerActivity activity) {
+ mReference = new WeakReference<AudioPlayerActivity>(activity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(MusicPlaybackService.META_CHANGED)) {
+ // Current info
+ mReference.get().updateNowPlayingInfo();
+ // Update the favorites icon
+ mReference.get().invalidateOptionsMenu();
+ } else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
+ // Set the play and pause image
+ mReference.get().mPlayPauseButton.updateState();
+ } else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
+ || action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
+ // Set the repeat image
+ mReference.get().mRepeatButton.updateRepeatState();
+ // Set the shuffle image
+ mReference.get().mShuffleButton.updateShuffleState();
+ }
+ }
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/activities/BaseActivity.java b/src/com/cyngn/eleven/ui/activities/BaseActivity.java
new file mode 100644
index 0000000..d9c4dab
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/BaseActivity.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import static com.cyngn.eleven.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.app.FragmentActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+import android.widget.TextView;
+
+import com.cyngn.eleven.IElevenService;
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.PlayPauseButton;
+import com.cyngn.eleven.widgets.RepeatButton;
+import com.cyngn.eleven.widgets.ShuffleButton;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * A base {@link FragmentActivity} used to update the bottom bar and
+ * bind to Apollo's service.
+ * <p>
+ * {@link HomeActivity} extends from this skeleton.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public abstract class BaseActivity extends FragmentActivity implements ServiceConnection {
+
+ /**
+ * Playstate and meta change listener
+ */
+ private final ArrayList<MusicStateListener> mMusicStateListener = Lists.newArrayList();
+
+ /**
+ * The service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * Play and pause button (BAB)
+ */
+ private PlayPauseButton mPlayPauseButton;
+
+ /**
+ * Repeat button (BAB)
+ */
+ private RepeatButton mRepeatButton;
+
+ /**
+ * Shuffle button (BAB)
+ */
+ private ShuffleButton mShuffleButton;
+
+ /**
+ * Track name (BAB)
+ */
+ private TextView mTrackName;
+
+ /**
+ * Artist name (BAB)
+ */
+ private TextView mArtistName;
+
+ /**
+ * Album art (BAB)
+ */
+ private ImageView mAlbumArt;
+
+ /**
+ * Broadcast receiver
+ */
+ private PlaybackStatus mPlaybackStatus;
+
+ /**
+ * Keeps track of the back button being used
+ */
+ private boolean mIsBackPressed = false;
+
+ /**
+ * Theme resources
+ */
+ protected ThemeUtils mResources;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Initialize the broadcast receiver
+ mPlaybackStatus = new PlaybackStatus(this);
+
+ // Theme the action bar
+ mResources.themeActionBar(getActionBar(), getString(R.string.app_name));
+
+ // Set the layout
+ setContentView(setContentView());
+
+ // Initialze the bottom action bar
+ initBottomActionBar();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IElevenService.Stub.asInterface(service);
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateBottomActionBarInfo();
+ // Update the favorites icon
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getMenuInflater().inflate(R.menu.search, menu);
+ // Settings
+ getMenuInflater().inflate(R.menu.activity_base, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ final SearchView searchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ searchView.setSearchableInfo(searchableInfo);
+ // Perform the search
+ searchView.setOnQueryTextListener(new OnQueryTextListener() {
+
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ // Open the search activity
+ NavUtils.openSearch(BaseActivity.this, query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ // Nothing to do
+ return false;
+ }
+ });
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_settings:
+ // Settings
+ NavUtils.openSettings(this);
+ return true;
+
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateBottomActionBarInfo();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final IntentFilter filter = new IntentFilter();
+ // Play and pause changes
+ filter.addAction(MusicPlaybackService.PLAYSTATE_CHANGED);
+ // Shuffle and repeat changes
+ filter.addAction(MusicPlaybackService.SHUFFLEMODE_CHANGED);
+ filter.addAction(MusicPlaybackService.REPEATMODE_CHANGED);
+ // Track changes
+ filter.addAction(MusicPlaybackService.META_CHANGED);
+ // Update a list, probably the playlist fragment's
+ filter.addAction(MusicPlaybackService.REFRESH);
+ registerReceiver(mPlaybackStatus, filter);
+ MusicUtils.notifyForegroundStateChanged(this, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ MusicUtils.notifyForegroundStateChanged(this, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mToken != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+
+ // Unregister the receiver
+ try {
+ unregisterReceiver(mPlaybackStatus);
+ } catch (final Throwable e) {
+ //$FALL-THROUGH$
+ }
+
+ // Remove any music status listeners
+ mMusicStateListener.clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ mIsBackPressed = true;
+ }
+
+ /**
+ * Initializes the items in the bottom action bar.
+ */
+ private void initBottomActionBar() {
+ // Play and pause button
+ mPlayPauseButton = (PlayPauseButton)findViewById(R.id.action_button_play);
+ // Shuffle button
+ mShuffleButton = (ShuffleButton)findViewById(R.id.action_button_shuffle);
+ // Repeat button
+ mRepeatButton = (RepeatButton)findViewById(R.id.action_button_repeat);
+ // Track name
+ mTrackName = (TextView)findViewById(R.id.bottom_action_bar_line_one);
+ // Artist name
+ mArtistName = (TextView)findViewById(R.id.bottom_action_bar_line_two);
+ // Album art
+ mAlbumArt = (ImageView)findViewById(R.id.bottom_action_bar_album_art);
+ // Open to the currently playing album profile
+ mAlbumArt.setOnClickListener(mOpenCurrentAlbumProfile);
+ // Bottom action bar
+ final LinearLayout bottomActionBar = (LinearLayout)findViewById(R.id.bottom_action_bar);
+ // Display the now playing screen or shuffle if this isn't anything
+ // playing
+ bottomActionBar.setOnClickListener(mOpenNowPlaying);
+ }
+
+ /**
+ * Sets the track name, album name, and album art.
+ */
+ private void updateBottomActionBarInfo() {
+ // Set the track name
+ mTrackName.setText(MusicUtils.getTrackName());
+ // Set the artist name
+ mArtistName.setText(MusicUtils.getArtistName());
+ // Set the album art
+ ApolloUtils.getImageFetcher(this).loadCurrentArtwork(mAlbumArt);
+ }
+
+ /**
+ * Sets the correct drawable states for the playback controls.
+ */
+ private void updatePlaybackControls() {
+ // Set the play and pause image
+ mPlayPauseButton.updateState();
+ // Set the shuffle image
+ mShuffleButton.updateShuffleState();
+ // Set the repeat image
+ mRepeatButton.updateRepeatState();
+ }
+
+ /**
+ * Opens the album profile of the currently playing album
+ */
+ private final View.OnClickListener mOpenCurrentAlbumProfile = new View.OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ if (MusicUtils.getCurrentAudioId() != -1) {
+ NavUtils.openAlbumProfile(BaseActivity.this, MusicUtils.getAlbumName(),
+ MusicUtils.getArtistName(), MusicUtils.getCurrentAlbumId());
+ } else {
+ MusicUtils.shuffleAll(BaseActivity.this);
+ }
+ if (BaseActivity.this instanceof ProfileActivity) {
+ finish();
+ }
+ }
+ };
+
+ /**
+ * Opens the now playing screen
+ */
+ private final View.OnClickListener mOpenNowPlaying = new View.OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ if (MusicUtils.getCurrentAudioId() != -1) {
+ NavUtils.openAudioPlayer(BaseActivity.this);
+ } else {
+ MusicUtils.shuffleAll(BaseActivity.this);
+ }
+ }
+ };
+
+ /**
+ * Used to monitor the state of playback
+ */
+ private final static class PlaybackStatus extends BroadcastReceiver {
+
+ private final WeakReference<BaseActivity> mReference;
+
+ /**
+ * Constructor of <code>PlaybackStatus</code>
+ */
+ public PlaybackStatus(final BaseActivity activity) {
+ mReference = new WeakReference<BaseActivity>(activity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(MusicPlaybackService.META_CHANGED)) {
+ // Current info
+ mReference.get().updateBottomActionBarInfo();
+ // Update the favorites icon
+ mReference.get().invalidateOptionsMenu();
+ // Let the listener know to the meta chnaged
+ for (final MusicStateListener listener : mReference.get().mMusicStateListener) {
+ if (listener != null) {
+ listener.onMetaChanged();
+ }
+ }
+ } else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
+ // Set the play and pause image
+ mReference.get().mPlayPauseButton.updateState();
+ } else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
+ || action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
+ // Set the repeat image
+ mReference.get().mRepeatButton.updateRepeatState();
+ // Set the shuffle image
+ mReference.get().mShuffleButton.updateShuffleState();
+ } else if (action.equals(MusicPlaybackService.REFRESH)) {
+ // Let the listener know to update a list
+ for (final MusicStateListener listener : mReference.get().mMusicStateListener) {
+ if (listener != null) {
+ listener.restartLoader();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param status The {@link MusicStateListener} to use
+ */
+ public void setMusicStateListenerListener(final MusicStateListener status) {
+ if (status != null) {
+ mMusicStateListener.add(status);
+ }
+ }
+
+ /**
+ * @return The resource ID to be inflated.
+ */
+ public abstract int setContentView();
+}
diff --git a/src/com/cyngn/eleven/ui/activities/HomeActivity.java b/src/com/cyngn/eleven/ui/activities/HomeActivity.java
new file mode 100644
index 0000000..0441e5a
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/HomeActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.ViewPager;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.fragments.phone.MusicBrowserPhoneFragment;
+
+/**
+ * This class is used to display the {@link ViewPager} used to swipe between the
+ * main {@link Fragment}s used to browse the user's music.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class HomeActivity extends BaseActivity {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the music browser fragment
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.activity_base_content, new MusicBrowserPhoneFragment()).commit();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_base;
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/activities/ProfileActivity.java b/src/com/cyngn/eleven/ui/activities/ProfileActivity.java
new file mode 100644
index 0000000..c224729
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/ProfileActivity.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.PagerAdapter;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.menu.PhotoSelectionDialog;
+import com.cyngn.eleven.menu.PhotoSelectionDialog.ProfileType;
+import com.cyngn.eleven.ui.fragments.profile.AlbumSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.ArtistAlbumFragment;
+import com.cyngn.eleven.ui.fragments.profile.ArtistSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.FavoriteFragment;
+import com.cyngn.eleven.ui.fragments.profile.GenreSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.LastAddedFragment;
+import com.cyngn.eleven.ui.fragments.profile.PlaylistSongFragment;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.utils.SortOrder;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.ProfileTabCarousel.Listener;
+
+/**
+ * The {@link Activity} is used to display the data for specific
+ * artists, albums, playlists, and genres. This class is only used on phones.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ProfileActivity extends BaseActivity implements OnPageChangeListener, Listener {
+
+ private static final int NEW_PHOTO = 1;
+
+ /**
+ * The Bundle to pass into the Fragments
+ */
+ private Bundle mArguments;
+
+ /**
+ * View pager
+ */
+ private ViewPager mViewPager;
+
+ /**
+ * Pager adpater
+ */
+ private PagerAdapter mPagerAdapter;
+
+ /**
+ * Profile header carousel
+ */
+ private ProfileTabCarousel mTabCarousel;
+
+ /**
+ * MIME type of the profile
+ */
+ private String mType;
+
+ /**
+ * Artist name passed into the class
+ */
+ private String mArtistName;
+
+ /**
+ * The main profile title
+ */
+ private String mProfileName;
+
+ /**
+ * Image cache
+ */
+ private ImageFetcher mImageFetcher;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Temporay until I can work out a nice landscape layout
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstance(this);
+
+ // Initialze the image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(this);
+
+ // Initialize the Bundle
+ mArguments = savedInstanceState != null ? savedInstanceState : getIntent().getExtras();
+ // Get the MIME type
+ mType = mArguments.getString(Config.MIME_TYPE);
+ // Get the profile title
+ mProfileName = mArguments.getString(Config.NAME);
+ // Get the artist name
+ if (isArtist() || isAlbum()) {
+ mArtistName = mArguments.getString(Config.ARTIST_NAME);
+ }
+
+ // Initialize the pager adapter
+ mPagerAdapter = new PagerAdapter(this);
+
+ // Initialze the carousel
+ mTabCarousel = (ProfileTabCarousel)findViewById(R.id.acivity_profile_base_tab_carousel);
+ mTabCarousel.reset();
+ mTabCarousel.getPhoto().setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ ProfileType profileType;
+ if (isArtist()) {
+ profileType = ProfileType.ARTIST;
+ } else if (isAlbum()) {
+ profileType = ProfileType.ALBUM;
+ } else {
+ profileType = ProfileType.OTHER;
+ }
+ PhotoSelectionDialog.newInstance(isArtist() ? mArtistName : mProfileName,
+ profileType).show(getSupportFragmentManager(), "PhotoSelectionDialog");
+ }
+ });
+ // Set up the action bar
+ final ActionBar actionBar = getActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ /* Set up the artist profile */
+ if (isArtist()) {
+ // Add the carousel images
+ mTabCarousel.setArtistProfileHeader(this, mArtistName);
+
+ // Artist profile fragments
+ mPagerAdapter.add(ArtistSongFragment.class, mArguments);
+ mPagerAdapter.add(ArtistAlbumFragment.class, mArguments);
+
+ // Action bar title
+ mResources.setTitle(mArtistName);
+
+ } else
+ // Set up the album profile
+ if (isAlbum()) {
+ // Add the carousel images
+ mTabCarousel.setAlbumProfileHeader(this, mProfileName, mArtistName);
+
+ // Album profile fragments
+ mPagerAdapter.add(AlbumSongFragment.class, mArguments);
+
+ // Action bar title = album name
+ mResources.setTitle(mProfileName);
+ // Action bar subtitle = year released
+ mResources.setSubtitle(mArguments.getString(Config.ALBUM_YEAR));
+ } else
+ // Set up the favorites profile
+ if (isFavorites()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Favorite fragment
+ mPagerAdapter.add(FavoriteFragment.class, null);
+
+ // Action bar title = Favorites
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the last added profile
+ if (isLastAdded()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Last added fragment
+ mPagerAdapter.add(LastAddedFragment.class, null);
+
+ // Action bar title = Last added
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the user playlist profile
+ if (isPlaylist()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Playlist profile fragments
+ mPagerAdapter.add(PlaylistSongFragment.class, mArguments);
+
+ // Action bar title = playlist name
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the genre profile
+ if (isGenre()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Genre profile fragments
+ mPagerAdapter.add(GenreSongFragment.class, mArguments);
+
+ // Action bar title = playlist name
+ mResources.setTitle(mProfileName);
+ }
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)findViewById(R.id.acivity_profile_base_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Attach the page change listener
+ mViewPager.setOnPageChangeListener(this);
+ // Attach the carousel listener
+ mTabCarousel.setListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mImageFetcher.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_profile_base;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ // Theme the add to home screen icon
+ mResources.setAddToHomeScreenIcon(menu);
+ // Set the shuffle all title to "play all" if a playlist.
+ final MenuItem shuffle = menu.findItem(R.id.menu_shuffle);
+ String title = null;
+ if (isFavorites() || isLastAdded() || isPlaylist()) {
+ title = getString(R.string.menu_play_all);
+ } else {
+ title = getString(R.string.menu_shuffle);
+ }
+ shuffle.setTitle(title);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Pin to Home screen
+ getMenuInflater().inflate(R.menu.add_to_homescreen, menu);
+ // Shuffle
+ getMenuInflater().inflate(R.menu.shuffle, menu);
+ // Sort orders
+ if (isArtistSongPage()) {
+ getMenuInflater().inflate(R.menu.artist_song_sort_by, menu);
+ } else if (isArtistAlbumPage()) {
+ getMenuInflater().inflate(R.menu.artist_album_sort_by, menu);
+ } else if (isAlbum()) {
+ getMenuInflater().inflate(R.menu.album_song_sort_by, menu);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // If an album profile, go up to the artist profile
+ if (isAlbum()) {
+ NavUtils.openArtistProfile(this, mArtistName);
+ finish();
+ } else {
+ // Otherwise just go back
+ goBack();
+ }
+ return true;
+ case R.id.menu_add_to_homescreen: {
+ // Place the artist, album, genre, or playlist onto the Home
+ // screen. Definitely one of my favorite features.
+ final String name = isArtist() ? mArtistName : mProfileName;
+ final Long id = mArguments.getLong(Config.ID);
+ ApolloUtils.createShortcutIntent(name, mArtistName, id, mType, this);
+ return true;
+ }
+ case R.id.menu_shuffle: {
+ final long id = mArguments.getLong(Config.ID);
+ long[] list = null;
+ if (isArtist()) {
+ list = MusicUtils.getSongListForArtist(this, id);
+ } else if (isAlbum()) {
+ list = MusicUtils.getSongListForAlbum(this, id);
+ } else if (isGenre()) {
+ list = MusicUtils.getSongListForGenre(this, id);
+ }
+ if (isPlaylist()) {
+ MusicUtils.playPlaylist(this, id);
+ } else if (isFavorites()) {
+ MusicUtils.playFavorites(this);
+ } else if (isLastAdded()) {
+ MusicUtils.playLastAdded(this);
+ } else {
+ if (list != null && list.length > 0) {
+ MusicUtils.playAll(this, list, 0, true);
+ }
+ }
+ return true;
+ }
+ case R.id.menu_sort_by_az:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_A_Z);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_A_Z);
+ getArtistAlbumFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_A_Z);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_za:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_Z_A);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_Z_A);
+ getArtistAlbumFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_Z_A);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_album:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_ALBUM);
+ getArtistSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_year:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_YEAR);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_YEAR);
+ getArtistAlbumFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_duration:
+ if (isArtistSongPage()) {
+ mPreferences
+ .setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_DURATION);
+ getArtistSongFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_DURATION);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_date_added:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_DATE);
+ getArtistSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_track_list:
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_TRACK_LIST);
+ getAlbumSongFragment().refresh();
+ return true;
+ case R.id.menu_sort_by_filename:
+ if(isArtistSongPage()) {
+ mPreferences.setArtistSortOrder(SortOrder.ArtistSongSortOrder.SONG_FILENAME);
+ getArtistSongFragment().refresh();
+ }
+ else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_FILENAME);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(mArguments);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ goBack();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageScrolled(final int position, final float positionOffset,
+ final int positionOffsetPixels) {
+ if (mViewPager.isFakeDragging()) {
+ return;
+ }
+
+ final int scrollToX = (int)((position + positionOffset) * mTabCarousel
+ .getAllowedHorizontalScrollLength());
+ mTabCarousel.scrollTo(scrollToX, 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageSelected(final int position) {
+ mTabCarousel.setCurrentTab(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageScrollStateChanged(final int state) {
+ if (state == ViewPager.SCROLL_STATE_IDLE) {
+ mTabCarousel.restoreYCoordinate(75, mViewPager.getCurrentItem());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTouchDown() {
+ if (!mViewPager.isFakeDragging()) {
+ mViewPager.beginFakeDrag();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTouchUp() {
+ if (mViewPager.isFakeDragging()) {
+ mViewPager.endFakeDrag();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
+ if (mViewPager.isFakeDragging()) {
+ mViewPager.fakeDragBy(oldl - l);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTabSelected(final int position) {
+ mViewPager.setCurrentItem(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == NEW_PHOTO) {
+ if (resultCode == RESULT_OK) {
+ final Uri selectedImage = data.getData();
+ final String[] filePathColumn = {
+ MediaStore.Images.Media.DATA
+ };
+
+ Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null,
+ null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
+ final String picturePath = cursor.getString(columnIndex);
+ cursor.close();
+ cursor = null;
+
+ String key = mProfileName;
+ if (isArtist()) {
+ key = mArtistName;
+ } else if (isAlbum()) {
+ key = ImageFetcher.generateAlbumCacheKey(mProfileName, mArtistName);
+ }
+
+ final Bitmap bitmap = ImageFetcher.decodeSampledBitmapFromFile(picturePath);
+ mImageFetcher.addBitmapToCache(key, bitmap);
+ if (isAlbum()) {
+ mTabCarousel.getAlbumArt().setImageBitmap(bitmap);
+ } else {
+ mTabCarousel.getPhoto().setImageBitmap(bitmap);
+ }
+ }
+ } else {
+ selectOldPhoto();
+ }
+ }
+ }
+
+ /**
+ * Starts an activity for result that returns an image from the Gallery.
+ */
+ public void selectNewPhoto() {
+ // First remove the old image
+ removeFromCache();
+ // Now open the gallery
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ startActivityForResult(intent, NEW_PHOTO);
+ }
+
+ /**
+ * Fetchs for the artist or album art, other wise sets the default header
+ * image.
+ */
+ public void selectOldPhoto() {
+ // First remove the old image
+ removeFromCache();
+ // Apply the old photo
+ if (isArtist()) {
+ mTabCarousel.setArtistProfileHeader(this, mArtistName);
+ } else if (isAlbum()) {
+ mTabCarousel.setAlbumProfileHeader(this, mProfileName, mArtistName);
+ } else {
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+ }
+ }
+
+ /**
+ * When the user chooses {@code #selectOldPhoto()} while viewing an album
+ * profile, the image is, most likely, reverted back to the locally found
+ * artwork. This is specifically for fetching the image from Last.fm.
+ */
+ public void fetchAlbumArt() {
+ // First remove the old image
+ removeFromCache();
+ // Fetch for the artwork
+ mTabCarousel.fetchAlbumPhoto(this, mProfileName, mArtistName);
+ }
+
+ /**
+ * Searches Google for the artist or album
+ */
+ public void googleSearch() {
+ String query = mProfileName;
+ if (isArtist()) {
+ query = mArtistName;
+ } else if (isAlbum()) {
+ query = mProfileName + " " + mArtistName;
+ }
+ final Intent googleSearch = new Intent(Intent.ACTION_WEB_SEARCH);
+ googleSearch.putExtra(SearchManager.QUERY, query);
+ startActivity(googleSearch);
+ }
+
+ /**
+ * Removes the header image from the cache.
+ */
+ private void removeFromCache() {
+ String key = mProfileName;
+ if (isArtist()) {
+ key = mArtistName;
+ } else if (isAlbum()) {
+ key = ImageFetcher.generateAlbumCacheKey(mProfileName, mArtistName);
+ }
+ mImageFetcher.removeFromCache(key);
+ // Give the disk cache a little time before requesting a new image.
+ SystemClock.sleep(80);
+ }
+
+ /**
+ * Finishes the activity and overrides the default animation.
+ */
+ private void goBack() {
+ finish();
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/artists, false
+ * otherwise.
+ */
+ private final boolean isArtist() {
+ return mType.equals(MediaStore.Audio.Artists.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/albums, false
+ * otherwise.
+ */
+ private final boolean isAlbum() {
+ return mType.equals(MediaStore.Audio.Albums.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/gere, false
+ * otherwise.
+ */
+ private final boolean isGenre() {
+ return mType.equals(MediaStore.Audio.Genres.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/playlist, false
+ * otherwise.
+ */
+ private final boolean isPlaylist() {
+ return mType.equals(MediaStore.Audio.Playlists.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is "Favorites", false otherwise.
+ */
+ private final boolean isFavorites() {
+ return mType.equals(getString(R.string.playlist_favorites));
+ }
+
+ /**
+ * @return True if the MIME type is "LastAdded", false otherwise.
+ */
+ private final boolean isLastAdded() {
+ return mType.equals(getString(R.string.playlist_last_added));
+ }
+
+ private boolean isArtistSongPage() {
+ return isArtist() && mViewPager.getCurrentItem() == 0;
+ }
+
+ private boolean isArtistAlbumPage() {
+ return isArtist() && mViewPager.getCurrentItem() == 1;
+ }
+
+ private ArtistSongFragment getArtistSongFragment() {
+ return (ArtistSongFragment)mPagerAdapter.getFragment(0);
+ }
+
+ private ArtistAlbumFragment getArtistAlbumFragment() {
+ return (ArtistAlbumFragment)mPagerAdapter.getFragment(1);
+ }
+
+ private AlbumSongFragment getAlbumSongFragment() {
+ return (AlbumSongFragment)mPagerAdapter.getFragment(0);
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/activities/SearchActivity.java b/src/com/cyngn/eleven/ui/activities/SearchActivity.java
new file mode 100644
index 0000000..2d775cb
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/SearchActivity.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import static com.cyngn.eleven.utils.MusicUtils.mService;
+
+import android.app.Activity;
+import android.app.ActionBar;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.CursorLoader;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.CursorAdapter;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ImageView.ScaleType;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+import android.widget.TextView;
+
+import com.cyngn.eleven.IElevenService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.format.PrefixHighlighter;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+
+import java.util.Locale;
+
+/**
+ * Provides the search interface for Apollo.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SearchActivity extends Activity implements LoaderCallbacks<Cursor>,
+ OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection {
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid
+ */
+ private static final int ONE = 1, TWO = 2;
+
+ /**
+ * The service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * The query
+ */
+ private String mFilterString;
+
+ /**
+ * Grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * List view adapter
+ */
+ private SearchAdapter mAdapter;
+
+ // Used the filter the user's music
+ private SearchView mSearchView;
+
+ /**
+ * Theme resources
+ */
+ private ThemeUtils mResources;
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Theme the action bar
+ final ActionBar actionBar = getActionBar();
+ mResources.themeActionBar(actionBar, getString(R.string.app_name));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ // Set the layout
+ setContentView(R.layout.grid_base);
+
+ // Give the background a little UI
+ final FrameLayout background = (FrameLayout)findViewById(R.id.grid_base_container);
+ background.setBackgroundDrawable(getResources().getDrawable(R.drawable.pager_background));
+
+ // Get the query
+ final String query = getIntent().getStringExtra(SearchManager.QUERY);
+ mFilterString = !TextUtils.isEmpty(query) ? query : null;
+
+ // Action bar subtitle
+ mResources.setSubtitle("\"" + mFilterString + "\"");
+
+ // Initialize the adapter
+ mAdapter = new SearchAdapter(this);
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ // Initialze the list
+ mGridView = (GridView)findViewById(R.id.grid_base);
+ // Bind the data
+ mGridView.setAdapter(mAdapter);
+ // Recycle the data
+ mGridView.setRecyclerListener(new RecycleHolder());
+ // Seepd up scrolling
+ mGridView.setOnScrollListener(this);
+ mGridView.setOnItemClickListener(this);
+ if (ApolloUtils.isLandscape(this)) {
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(ONE);
+ }
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onNewIntent(final Intent intent) {
+ super.onNewIntent(intent);
+ final String query = intent.getStringExtra(SearchManager.QUERY);
+ mFilterString = !TextUtils.isEmpty(query) ? query : null;
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ getLoaderManager().restartLoader(0, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getMenuInflater().inflate(R.menu.search, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ // Filter the list the user is looking it via SearchView
+ mSearchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ mSearchView.setOnQueryTextListener(this);
+
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ mSearchView.setSearchableInfo(searchableInfo);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ MusicUtils.notifyForegroundStateChanged(this, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ MusicUtils.notifyForegroundStateChanged(this, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ final Uri uri = Uri.parse("content://media/external/audio/search/fancy/"
+ + Uri.encode(mFilterString));
+ final String[] projection = new String[] {
+ BaseColumns._ID, MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Artists.ARTIST,
+ MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Media.TITLE, "data1", "data2"
+ };
+ return new CursorLoader(this, uri, projection, null, null, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
+ if (data == null || data.isClosed() || data.getCount() <= 0) {
+ // Set the empty text
+ final TextView empty = (TextView)findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_search));
+ mGridView.setEmptyView(empty);
+ return;
+ }
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.swapCursor(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ if (TextUtils.isEmpty(query)) {
+ return false;
+ }
+ // When the search is "committed" by the user, then hide the keyboard so
+ // the user can
+ // more easily browse the list of results.
+ if (mSearchView != null) {
+ final InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
+ }
+ mSearchView.clearFocus();
+ }
+ // Action bar subtitle
+ mResources.setSubtitle("\"" + mFilterString + "\"");
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ if (TextUtils.isEmpty(newText)) {
+ return false;
+ }
+ // Called when the action bar search text has changed. Update
+ // the search filter, and restart the loader to do a new query
+ // with this filter.
+ mFilterString = !TextUtils.isEmpty(newText) ? newText : null;
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ getLoaderManager().restartLoader(0, null, this);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ Cursor cursor = mAdapter.getCursor();
+ cursor.moveToPosition(position);
+ if (cursor.isBeforeFirst() || cursor.isAfterLast()) {
+ return;
+ }
+ // Get the MIME type
+ final String mimeType = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
+
+ // If it's an artist, open the artist profile
+ if ("artist".equals(mimeType)) {
+ NavUtils.openArtistProfile(this,
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)));
+ } else if ("album".equals(mimeType)) {
+ // If it's an album, open the album profile
+ NavUtils.openAlbumProfile(this,
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)),
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST)),
+ cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID)));
+ } else if (position >= 0 && id >= 0) {
+ // If it's a song, play it and leave
+ final long[] list = new long[] {
+ id
+ };
+ MusicUtils.playAll(this, list, 0, false);
+ }
+
+ // Close it up
+ cursor.close();
+ cursor = null;
+ // All done
+ finish();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IElevenService.Stub.asInterface(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * Used to populate the list view with the search results.
+ */
+ private static final class SearchAdapter extends CursorAdapter {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Highlights the query
+ */
+ private final PrefixHighlighter mHighlighter;
+
+ /**
+ * The prefix that's highlighted
+ */
+ private char[] mPrefix;
+
+ /**
+ * Constructor for <code>SearchAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public SearchAdapter(final Activity context) {
+ super(context, null, false);
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(context);
+ // Create the prefix highlighter
+ mHighlighter = new PrefixHighlighter(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void bindView(final View convertView, final Context context, final Cursor cursor) {
+ /* Recycle ViewHolder's items */
+ MusicHolder holder = (MusicHolder)convertView.getTag();
+ if (holder == null) {
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ }
+
+ // Get the MIME type
+ final String mimetype = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
+
+ if (mimetype.equals("artist")) {
+ holder.mImage.get().setScaleType(ScaleType.CENTER_CROP);
+
+ // Get the artist name
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+ holder.mLineOne.get().setText(artist);
+
+ // Get the album count
+ final int albumCount = cursor.getInt(cursor.getColumnIndexOrThrow("data1"));
+ holder.mLineTwo.get().setText(
+ MusicUtils.makeLabel(context, R.plurals.Nalbums, albumCount));
+
+ // Get the song count
+ final int songCount = cursor.getInt(cursor.getColumnIndexOrThrow("data2"));
+ holder.mLineThree.get().setText(
+ MusicUtils.makeLabel(context, R.plurals.Nsongs, songCount));
+
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mImage.get());
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), artist, mPrefix);
+ } else if (mimetype.equals("album")) {
+ holder.mImage.get().setScaleType(ScaleType.FIT_XY);
+
+ // Get the Id of the album
+ final long id = cursor.getLong(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
+
+ // Get the album name
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+ holder.mLineOne.get().setText(album);
+
+ // Get the artist name
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
+ holder.mLineTwo.get().setText(artist);
+
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(artist, album, id, holder.mImage.get());
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), album, mPrefix);
+
+ } else if (mimetype.startsWith("audio/") || mimetype.equals("application/ogg")
+ || mimetype.equals("application/x-ogg")) {
+ holder.mImage.get().setScaleType(ScaleType.FIT_XY);
+ holder.mImage.get().setImageResource(R.drawable.header_temp);
+
+ // Get the track name
+ final String track = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
+ holder.mLineOne.get().setText(track);
+
+ // Get the album name
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
+ holder.mLineTwo.get().setText(album);
+
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
+ holder.mLineThree.get().setText(artist);
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), track, mPrefix);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
+ return ((Activity)context).getLayoutInflater().inflate(
+ R.layout.list_item_detailed, parent, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false
+ * otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param prefix The query to filter.
+ */
+ public void setPrefix(final CharSequence prefix) {
+ if (!TextUtils.isEmpty(prefix)) {
+ mPrefix = prefix.toString().toUpperCase(Locale.getDefault()).toCharArray();
+ } else {
+ mPrefix = null;
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/activities/SettingsActivity.java b/src/com/cyngn/eleven/ui/activities/SettingsActivity.java
new file mode 100644
index 0000000..3ddda90
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/SettingsActivity.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceActivity;
+import android.view.MenuItem;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.ui.fragments.ThemeFragment;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.widgets.ColorSchemeDialog;
+
+/**
+ * Settings.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressWarnings("deprecation")
+public class SettingsActivity extends PreferenceActivity {
+
+ /**
+ * Image cache
+ */
+ private ImageCache mImageCache;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstance(this);
+
+ // Initialze the image cache
+ mImageCache = ImageCache.getInstance(this);
+
+ // UP
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Add the preferences
+ addPreferencesFromResource(R.xml.settings);
+
+ // Interface settings
+ initInterface();
+ // Removes the cache entries
+ deleteCache();
+ // About
+ showOpenSourceLicenses();
+ // Update the version number
+ try {
+ final PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ findPreference("version").setSummary(packageInfo.versionName);
+ } catch (final NameNotFoundException e) {
+ findPreference("version").setSummary("?");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ finish();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ MusicUtils.notifyForegroundStateChanged(this, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ MusicUtils.notifyForegroundStateChanged(this, false);
+ }
+
+ /**
+ * Initializes the preferences under the "Interface" category
+ */
+ private void initInterface() {
+ // Color scheme picker
+ updateColorScheme();
+ // Open the theme chooser
+ openThemeChooser();
+ }
+
+ /**
+ * Shows the {@link ColorSchemeDialog} and then saves the changes.
+ */
+ private void updateColorScheme() {
+ final Preference colorScheme = findPreference("color_scheme");
+ colorScheme.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ ApolloUtils.showColorPicker(SettingsActivity.this);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Opens the {@link ThemeFragment}.
+ */
+ private void openThemeChooser() {
+ final Preference themeChooser = findPreference("theme_chooser");
+ themeChooser.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ final Intent themeChooserIntent = new Intent(SettingsActivity.this,
+ ThemesActivity.class);
+ startActivity(themeChooserIntent);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Removes all of the cache entries.
+ */
+ private void deleteCache() {
+ final Preference deleteCache = findPreference("delete_cache");
+ deleteCache.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ new AlertDialog.Builder(SettingsActivity.this).setMessage(R.string.delete_warning)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ mImageCache.clearCaches();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).create().show();
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Show the open source licenses
+ */
+ private void showOpenSourceLicenses() {
+ final Preference mOpenSourceLicenses = findPreference("open_source");
+ mOpenSourceLicenses.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ ApolloUtils.createOpenSourceDialog(SettingsActivity.this).show();
+ return true;
+ }
+ });
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/activities/ShortcutActivity.java b/src/com/cyngn/eleven/ui/activities/ShortcutActivity.java
new file mode 100644
index 0000000..b9a1731
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/ShortcutActivity.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import static com.cyngn.eleven.Config.MIME_TYPE;
+import static com.cyngn.eleven.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.MediaStore;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.IElevenService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.format.Capitalize;
+import com.cyngn.eleven.loaders.AsyncHandler;
+import com.cyngn.eleven.loaders.LastAddedLoader;
+import com.cyngn.eleven.loaders.SearchLoader;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is opened when the user touches a Home screen shortcut or album
+ * art in an app-wdget, and then carries out the proper action. It is also
+ * responsible for processing voice queries and playing the spoken artist,
+ * album, song, playlist, or genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ShortcutActivity extends FragmentActivity implements ServiceConnection {
+
+ /**
+ * If true, this class will begin playback and open
+ * {@link AudioPlayerActivity}, false will close the class after playback,
+ * which is what happens when a user starts playing something from an
+ * app-widget
+ */
+ public static final String OPEN_AUDIO_PLAYER = null;
+
+ /**
+ * Service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * Gather the intent action and extras
+ */
+ private Intent mIntent;
+
+ /**
+ * The list of songs to play
+ */
+ private long[] mList;
+
+ /**
+ * Used to shuffle the tracks or play them in order
+ */
+ private boolean mShouldShuffle;
+
+ /**
+ * Search query from a voice action
+ */
+ private String mVoiceQuery;
+
+ /**
+ * Used with the loader and voice queries
+ */
+ private final ArrayList<Song> mSong = Lists.newArrayList();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Intiialize the intent
+ mIntent = getIntent();
+ // Get the voice search query
+ mVoiceQuery = Capitalize.capitalize(mIntent.getStringExtra(SearchManager.QUERY));
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IElevenService.Stub.asInterface(service);
+
+ // Check for a voice query
+ if (mIntent.getAction().equals(Config.PLAY_FROM_SEARCH)) {
+ getSupportLoaderManager().initLoader(0, null, mSongAlbumArtistQuery);
+ } else if (mService != null) {
+ AsyncHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final String requestedMimeType = mIntent.getExtras().getString(MIME_TYPE);
+
+ // First, check the artist MIME type
+ if (MediaStore.Audio.Artists.CONTENT_TYPE.equals(requestedMimeType)) {
+
+ // Shuffle the artist track list
+ mShouldShuffle = true;
+
+ // Get the artist song list
+ mList = MusicUtils.getSongListForArtist(ShortcutActivity.this, getId());
+ } else
+ // Second, check the album MIME type
+ if (MediaStore.Audio.Albums.CONTENT_TYPE.equals(requestedMimeType)) {
+
+ // Shuffle the album track list
+ mShouldShuffle = true;
+
+ // Get the album song list
+ mList = MusicUtils.getSongListForAlbum(ShortcutActivity.this, getId());
+ } else
+ // Third, check the genre MIME type
+ if (MediaStore.Audio.Genres.CONTENT_TYPE.equals(requestedMimeType)) {
+
+ // Shuffle the genre track list
+ mShouldShuffle = true;
+
+ // Get the genre song list
+ mList = MusicUtils.getSongListForGenre(ShortcutActivity.this, getId());
+ } else
+ // Fourth, check the playlist MIME type
+ if (MediaStore.Audio.Playlists.CONTENT_TYPE.equals(requestedMimeType)) {
+
+ // Don't shuffle the playlist track list
+ mShouldShuffle = false;
+
+ // Get the playlist song list
+ mList = MusicUtils.getSongListForPlaylist(ShortcutActivity.this, getId());
+ } else
+ // Check the Favorites playlist
+ if (getString(R.string.playlist_favorites).equals(requestedMimeType)) {
+
+ // Don't shuffle the Favorites track list
+ mShouldShuffle = false;
+
+ // Get the Favorites song list
+ mList = MusicUtils.getSongListForFavorites(ShortcutActivity.this);
+ } else
+ // Check for the Last added playlist
+ if (getString(R.string.playlist_last_added).equals(requestedMimeType)) {
+
+ // Don't shuffle the last added track list
+ mShouldShuffle = false;
+
+ // Get the Last added song list
+ Cursor cursor = LastAddedLoader.makeLastAddedCursor(ShortcutActivity.this);
+ if (cursor != null) {
+ mList = MusicUtils.getSongListForCursor(cursor);
+ cursor.close();
+ }
+ }
+ // Finish up
+ allDone();
+ }
+ });
+ } else {
+ // TODO Show and error explaining why
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+ }
+
+ /**
+ * Uses the query from a voice search to try and play a song, then album,
+ * then artist. If all of those fail, it checks for playlists and genres via
+ * a {@link #mPlaylistGenreQuery}.
+ */
+ private final LoaderCallbacks<List<Song>> mSongAlbumArtistQuery = new LoaderCallbacks<List<Song>>() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new SearchLoader(ShortcutActivity.this, mVoiceQuery);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // If the user searched for a playlist or genre, this list will
+ // return empty
+ if (data.isEmpty()) {
+ // Before running the playlist loader, try to play the
+ // "Favorites" playlist
+ if (isFavorite()) {
+ MusicUtils.playFavorites(ShortcutActivity.this);
+ }
+ // Finish up
+ allDone();
+ return;
+ }
+
+ // Start fresh
+ mSong.clear();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mSong.add(song);
+ }
+
+ // What's about to happen is similar to the above process. Apollo
+ // runs a
+ // series of checks to see if anything comes up. When it does, it
+ // assumes (pretty accurately) that it should begin to play that
+ // thing.
+ // The fancy search query used in {@link SearchLoader} is the key to
+ // this. It allows the user to perform very specific queries. i.e.
+ // "Listen to Ethio
+
+ final String song = mSong.get(0).mSongName;
+ final String album = mSong.get(0).mAlbumName;
+ final String artist = mSong.get(0).mArtistName;
+ // This tripes as the song, album, and artist Id
+ final long id = mSong.get(0).mSongId;
+ // First, try to play a song
+ if (mList == null && song != null) {
+ mList = new long[] {
+ id
+ };
+ } else
+ // Second, try to play an album
+ if (mList == null && album != null) {
+ mList = MusicUtils.getSongListForAlbum(ShortcutActivity.this, id);
+ } else
+ // Third, try to play an artist
+ if (mList == null && artist != null) {
+ mList = MusicUtils.getSongListForArtist(ShortcutActivity.this, id);
+ }
+ // Finish up
+ allDone();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data
+ mSong.clear();
+ }
+ };
+
+ /**
+ * Used to find the Id supplied
+ *
+ * @return The Id passed into the activity
+ */
+ private long getId() {
+ return mIntent.getExtras().getLong(Config.ID);
+ }
+
+ /**
+ * @return True if the user searched for the favorites playlist
+ */
+ private boolean isFavorite() {
+ // Check to see if the user spoke the word "Favorites"
+ final String favoritePlaylist = getString(R.string.playlist_favorites);
+ if (mVoiceQuery.equals(favoritePlaylist)) {
+ return true;
+ }
+
+ // Check to see if the user spoke the word "Favorite"
+ final String favorite = getString(R.string.playlist_favorite);
+ if (mVoiceQuery.equals(favorite)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Starts playback, open {@link AudioPlayerActivity} and finishes this one
+ */
+ private void allDone() {
+ final boolean shouldOpenAudioPlayer = mIntent.getBooleanExtra(OPEN_AUDIO_PLAYER, true);
+ // Play the list
+ if (mList != null && mList.length > 0) {
+ MusicUtils.playAll(this, mList, 0, mShouldShuffle);
+ }
+
+ // Open the now playing screen
+ if (shouldOpenAudioPlayer) {
+ final Intent intent = new Intent(this, AudioPlayerActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ // All done
+ finish();
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/activities/ThemesActivity.java b/src/com/cyngn/eleven/ui/activities/ThemesActivity.java
new file mode 100644
index 0000000..712f1fb
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/activities/ThemesActivity.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.activities;
+
+import android.app.ActionBar;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.fragments.ThemeFragment;
+
+/**
+ * A class the displays the {@link ThemeFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemesActivity extends BaseActivity {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Set up the action bar
+ final ActionBar actionBar = getActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(getString(R.string.settings_theme_chooser_title));
+
+ // Transact the theme fragment
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.activity_base_content, new ThemeFragment()).commit();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ mResources.setShopIcon(menu);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.theme_shop, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.menu_shop:
+ mResources.shopFor(this);
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_base;
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/AlbumFragment.java b/src/com/cyngn/eleven/ui/fragments/AlbumFragment.java
new file mode 100644
index 0000000..ebeb566
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/AlbumFragment.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import static com.cyngn.eleven.utils.PreferenceUtils.ALBUM_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.AlbumAdapter;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.loaders.AlbumLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.BaseActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the albums on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Album>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 3;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private AlbumAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int layout = R.layout.list_item_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_normal;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new AlbumAdapter(getActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the album artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Remove the album from the list
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mAlbum.mArtistName);
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String album = mAlbum.mAlbumName;
+ DeleteDialog.newInstance(album, mAlbumList,
+ ImageFetcher.generateAlbumCacheKey(album,mAlbum.mArtistName))
+ .show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mAlbum = mAdapter.getItem(position);
+ NavUtils.openAlbumProfile(getActivity(), mAlbum.mAlbumName, mAlbum.mArtistName, mAlbum.mAlbumId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new AlbumLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing album when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentAlbum() {
+ final int currentAlbumPosition = getItemPositionByAlbum();
+
+ if (currentAlbumPosition != 0) {
+ if (isSimpleLayout()) {
+ mListView.setSelection(currentAlbumPosition);
+ } else {
+ mGridView.setSelection(currentAlbumPosition);
+ }
+ }
+ }
+
+ /**
+ * @return The position of an item in the list or grid based on the id of
+ * the currently playing album.
+ */
+ private int getItemPositionByAlbum() {
+ final long albumId = MusicUtils.getCurrentAlbumId();
+ if (mAdapter == null) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mAlbumId == albumId) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ mAdapter.setTouchPlay(true);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isSimpleLayout(ALBUM_LAYOUT,
+ getActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isDetailedLayout(ALBUM_LAYOUT,
+ getActivity());
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/ArtistFragment.java b/src/com/cyngn/eleven/ui/fragments/ArtistFragment.java
new file mode 100644
index 0000000..c007a03
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/ArtistFragment.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import static com.cyngn.eleven.utils.PreferenceUtils.ARTIST_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ArtistAdapter;
+import com.cyngn.eleven.loaders.ArtistLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.BaseActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the artists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Artist>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 2;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private ArtistAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Artist song list
+ */
+ private long[] mArtistList;
+
+ /**
+ * Represents an artist
+ */
+ private Artist mArtist;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ int layout = R.layout.grid_items_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_simple;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new ArtistAdapter(getActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Creat a new model
+ mArtist = mAdapter.getItem(info.position);
+ // Create a list of the artist's songs
+ mArtistList = MusicUtils.getSongListForArtist(getActivity(), mArtist.mArtistId);
+
+ // Play the artist
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the artist to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the artist to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // Delete the artist
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), mArtistList, 0, true);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), mArtistList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mArtistList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), mArtistList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String artist = mArtist.mArtistName;
+ DeleteDialog.newInstance(artist, mArtistList, artist).show(
+ getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mArtist = mAdapter.getItem(position);
+ NavUtils.openArtistProfile(getActivity(), mArtist.mArtistName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Artist>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Artist>> loader, final List<Artist> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Artist artist : data) {
+ mAdapter.add(artist);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Artist>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing artist when the user touches
+ * the header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentArtist() {
+ final int currentArtistPosition = getItemPositionByArtist();
+
+ if (currentArtistPosition != 0) {
+ if (isSimpleLayout()) {
+ mListView.setSelection(currentArtistPosition);
+ } else {
+ mGridView.setSelection(currentArtistPosition);
+ }
+ }
+ }
+
+ /**
+ * @return The position of an item in the list or grid based on the name of
+ * the currently playing artist.
+ */
+ private int getItemPositionByArtist() {
+ final long artistId = MusicUtils.getCurrentArtistId();
+ if (mAdapter == null) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mArtistId == artistId) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isSimpleLayout(ARTIST_LAYOUT,
+ getActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isDetailedLayout(ARTIST_LAYOUT,
+ getActivity());
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/GenreFragment.java b/src/com/cyngn/eleven/ui/fragments/GenreFragment.java
new file mode 100644
index 0000000..49b1c18
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/GenreFragment.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.GenreAdapter;
+import com.cyngn.eleven.loaders.GenreLoader;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Genre;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the genres on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreFragment extends Fragment implements LoaderCallbacks<List<Genre>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 5;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private GenreAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Genre song list
+ */
+ private long[] mGenreList;
+
+ /**
+ * Represents a genre
+ */
+ private Genre mGenre;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public GenreFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new GenreAdapter(getActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected genre
+ mListView.setOnItemClickListener(this);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new genre
+ mGenre = mAdapter.getItem(info.position);
+ // Create a list of the genre's songs
+ mGenreList = MusicUtils.getSongListForGenre(getActivity(), mGenre.mGenreId);
+
+ // Play the genre
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ R.string.context_menu_play_selection);
+ // Add the genre to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE, R.string.add_to_queue);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), mGenreList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), mGenreList);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mGenre = mAdapter.getItem(position);
+ // Create a new bundle to transfer the artist info
+ final Bundle bundle = new Bundle();
+ bundle.putLong(Config.ID, mGenre.mGenreId);
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Genres.CONTENT_TYPE);
+ bundle.putString(Config.NAME, mGenre.mGenreName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(getActivity(), ProfileActivity.class);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Genre>> onCreateLoader(final int id, final Bundle args) {
+ return new GenreLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Genre>> loader, final List<Genre> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Genre genre : data) {
+ mAdapter.add(genre);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Genre>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/PlaylistFragment.java b/src/com/cyngn/eleven/ui/fragments/PlaylistFragment.java
new file mode 100644
index 0000000..dc2e4eb
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/PlaylistFragment.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.PlaylistAdapter;
+import com.cyngn.eleven.loaders.PlaylistLoader;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.menu.RenamePlaylist;
+import com.cyngn.eleven.model.Playlist;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.BaseActivity;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the playlists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistFragment extends Fragment implements LoaderCallbacks<List<Playlist>>,
+ OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 0;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private PlaylistAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a playlist
+ */
+ private Playlist mPlaylist;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PlaylistFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new PlaylistAdapter(getActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the grid
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ final int mPosition = info.position;
+ // Create a new playlist
+ mPlaylist = mAdapter.getItem(mPosition);
+
+ // Play the playlist
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ R.string.context_menu_play_selection);
+
+ // Add the playlist to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE, R.string.add_to_queue);
+
+ // Delete and rename (user made playlists)
+ if (info.position > 1) {
+ menu.add(GROUP_ID, FragmentMenuItems.RENAME_PLAYLIST, Menu.NONE,
+ R.string.context_menu_rename_playlist);
+
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE, R.string.context_menu_delete);
+
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ if (info.position == 0) {
+ MusicUtils.playFavorites(getActivity());
+ } else if (info.position == 1) {
+ MusicUtils.playLastAdded(getActivity());
+ } else {
+ MusicUtils.playPlaylist(getActivity(), mPlaylist.mPlaylistId);
+ }
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ long[] list = null;
+ if (info.position == 0) {
+ list = MusicUtils.getSongListForFavorites(getActivity());
+ } else if (info.position == 1) {
+ list = MusicUtils.getSongListForLastAdded(getActivity());
+ } else {
+ list = MusicUtils.getSongListForPlaylist(getActivity(),
+ mPlaylist.mPlaylistId);
+ }
+ MusicUtils.addToQueue(getActivity(), list);
+ return true;
+ case FragmentMenuItems.RENAME_PLAYLIST:
+ RenamePlaylist.getInstance(mPlaylist.mPlaylistId).show(
+ getFragmentManager(), "RenameDialog");
+ return true;
+ case FragmentMenuItems.DELETE:
+ buildDeleteDialog().show();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ final Bundle bundle = new Bundle();
+ mPlaylist = mAdapter.getItem(position);
+ String playlistName;
+ // Favorites list
+ if (position == 0) {
+ playlistName = getString(R.string.playlist_favorites);
+ bundle.putString(Config.MIME_TYPE, getString(R.string.playlist_favorites));
+ // Last added
+ } else if (position == 1) {
+ playlistName = getString(R.string.playlist_last_added);
+ bundle.putString(Config.MIME_TYPE, getString(R.string.playlist_last_added));
+ } else {
+ // User created
+ playlistName = mPlaylist.mPlaylistName;
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Playlists.CONTENT_TYPE);
+ bundle.putLong(Config.ID, mPlaylist.mPlaylistId);
+ }
+
+ bundle.putString(Config.NAME, playlistName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(getActivity(), ProfileActivity.class);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Playlist>> onCreateLoader(final int id, final Bundle args) {
+ return new PlaylistLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Playlist>> loader, final List<Playlist> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Playlist playlist : data) {
+ mAdapter.add(playlist);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Playlist>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Refresh the list when a playlist is deleted or renamed
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Create a new {@link AlertDialog} for easy playlist deletion
+ *
+ * @param context The {@link Context} to use
+ * @param title The title of the playlist being deleted
+ * @param id The ID of the playlist being deleted
+ * @return A new {@link AlertDialog} used to delete playlists
+ */
+ private final AlertDialog buildDeleteDialog() {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.delete_dialog_title, mPlaylist.mPlaylistName))
+ .setPositiveButton(R.string.context_menu_delete, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ final Uri mUri = ContentUris.withAppendedId(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ mPlaylist.mPlaylistId);
+ getActivity().getContentResolver().delete(mUri, null, null);
+ MusicUtils.refresh();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).setMessage(R.string.cannot_be_undone).create();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/QueueFragment.java b/src/com/cyngn/eleven/ui/fragments/QueueFragment.java
new file mode 100644
index 0000000..cba9743
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/QueueFragment.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.SongAdapter;
+import com.cyngn.eleven.dragdrop.DragSortListView;
+import com.cyngn.eleven.dragdrop.DragSortListView.DragScrollProfile;
+import com.cyngn.eleven.dragdrop.DragSortListView.DropListener;
+import com.cyngn.eleven.dragdrop.DragSortListView.RemoveListener;
+import com.cyngn.eleven.loaders.NowPlayingCursor;
+import com.cyngn.eleven.loaders.QueueLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs in the queue.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class QueueFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, DropListener, RemoveListener, DragScrollProfile {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 13;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private SongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private DragSortListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public QueueFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new SongAdapter(getActivity(), R.layout.edit_track_list_item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (DragSortListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // Set the drop listener
+ mListView.setDropListener(this);
+ // Set the swipe to remove listener
+ mListView.setRemoveListener(this);
+ // Quick scroll while dragging
+ mListView.setDragScrollProfile(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.queue, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_save_queue:
+ NowPlayingCursor queue = (NowPlayingCursor)QueueLoader
+ .makeQueueCursor(getActivity());
+ CreateNewPlaylist.getInstance(MusicUtils.getSongListForCursor(queue)).show(
+ getFragmentManager(), "CreatePlaylist");
+ queue.close();
+ queue = null;
+ return true;
+ case R.id.menu_clear_queue:
+ MusicUtils.clearQueue();
+ NavUtils.goHome(getActivity());
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // Remove the song from the queue
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_QUEUE, Menu.NONE,
+ getString(R.string.remove_from_queue));
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_NEXT:
+ NowPlayingCursor queue = (NowPlayingCursor)QueueLoader
+ .makeQueueCursor(getActivity());
+ queue.removeItem(mSelectedPosition);
+ queue.close();
+ queue = null;
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ refreshQueue();
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_QUEUE:
+ MusicUtils.removeTrack(mSelectedId);
+ refreshQueue();
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ // When selecting a track from the queue, just jump there instead of
+ // reloading the queue. This is both faster, and prevents accidentally
+ // dropping out of party shuffle.
+ MusicUtils.setQueuePosition(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new QueueLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ if (w > 0.8f) {
+ return mAdapter.getCount() / 0.001f;
+ } else {
+ return 10.0f * w;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remove(final int which) {
+ mSong = mAdapter.getItem(which);
+ mAdapter.remove(mSong);
+ mAdapter.notifyDataSetChanged();
+ MusicUtils.removeTrack(mSong.mSongId);
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final int from, final int to) {
+ mSong = mAdapter.getItem(from);
+ mAdapter.remove(mSong);
+ mAdapter.insert(mSong, to);
+ mAdapter.notifyDataSetChanged();
+ MusicUtils.moveQueueItem(from, to);
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * Scrolls the list to the currently playing song when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentSong() {
+ final int currentSongPosition = getItemPositionBySong();
+
+ if (currentSongPosition != 0) {
+ mListView.setSelection(currentSongPosition);
+ }
+ }
+
+ /**
+ * @return The position of an item in the list based on the name of the
+ * currently playing song.
+ */
+ private int getItemPositionBySong() {
+ final long trackId = MusicUtils.getCurrentAudioId();
+ if (mAdapter == null) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mSongId == trackId) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called to restart the loader callbacks
+ */
+ public void refreshQueue() {
+ if (isAdded()) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/RecentFragment.java b/src/com/cyngn/eleven/ui/fragments/RecentFragment.java
new file mode 100644
index 0000000..c4684e1
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/RecentFragment.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import static com.cyngn.eleven.utils.PreferenceUtils.RECENT_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.AlbumAdapter;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.loaders.RecentLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.provider.RecentStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.BaseActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the recently listened to albums by the
+ * user.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentFragment extends Fragment implements LoaderCallbacks<List<Album>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 1;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private AlbumAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int layout = R.layout.list_item_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_normal;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new AlbumAdapter(getActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the album artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Remove the album from the list
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_RECENT, Menu.NONE,
+ getString(R.string.context_menu_remove_from_recent));
+
+ // Delete the album
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mAlbum.mArtistName);
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_RECENT:
+ mShouldRefresh = true;
+ RecentStore.getInstance(getActivity()).removeItem(mAlbum.mAlbumId);
+ MusicUtils.refresh();
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String album = mAlbum.mAlbumName;
+ DeleteDialog.newInstance(album, mAlbumList,
+ ImageFetcher.generateAlbumCacheKey(album, mAlbum.mArtistName))
+ .show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mAlbum = mAdapter.getItem(position);
+ NavUtils.openAlbumProfile(getActivity(), mAlbum.mAlbumName, mAlbum.mArtistName, mAlbum.mAlbumId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new RecentLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_recent));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ mAdapter.setTouchPlay(true);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isSimpleLayout(RECENT_LAYOUT,
+ getActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstance(getActivity()).isDetailedLayout(RECENT_LAYOUT,
+ getActivity());
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/SongFragment.java b/src/com/cyngn/eleven/ui/fragments/SongFragment.java
new file mode 100644
index 0000000..bb8dece
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/SongFragment.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.MusicStateListener;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.SongAdapter;
+import com.cyngn.eleven.loaders.SongLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.activities.BaseActivity;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private SongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public SongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new SongAdapter(getActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ Cursor cursor = SongLoader.makeSongCursor(getActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new SongLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing song when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentSong() {
+ final int currentSongPosition = getItemPositionBySong();
+
+ if (currentSongPosition != 0) {
+ mListView.setSelection(currentSongPosition);
+ }
+ }
+
+ /**
+ * @return The position of an item in the list based on the name of the
+ * currently playing song.
+ */
+ private int getItemPositionBySong() {
+ final long trackId = MusicUtils.getCurrentAudioId();
+ if (mAdapter == null) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mSongId == trackId) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/ThemeFragment.java b/src/com/cyngn/eleven/ui/fragments/ThemeFragment.java
new file mode 100644
index 0000000..a4ad28a
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/ThemeFragment.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.GridView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.ui.MusicHolder;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.devspark.appmsg.AppMsg;
+
+import java.util.List;
+
+/**
+ * Used to show all of the available themes on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeFragment extends Fragment implements OnItemClickListener {
+
+ private static final int OPEN_IN_PLAY_STORE = 0;
+
+ private GridView mGridView;
+
+ private PackageManager mPackageManager;
+
+ private List<ResolveInfo> mThemes;
+
+ private String[] mEntries;
+
+ private String[] mValues;
+
+ private Drawable[] mThemePreview;
+
+ private Resources mThemeResources;
+
+ private String mThemePackageName;
+
+ private String mThemeName;
+
+ private ThemesAdapter mAdapter;
+
+ private ThemeUtils mTheme;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ThemeFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ // Initialize the grid
+ mGridView = (GridView)rootView.findViewById(R.id.grid_base);
+ // Release any reference to the recycled Views
+ mGridView.setRecyclerListener(new RecycleHolder());
+ // Set the new theme
+ mGridView.setOnItemClickListener(this);
+ // Listen for ContextMenus to be created
+ mGridView.setOnCreateContextMenuListener(this);
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ // Limit the columns to one in portrait mode
+ mGridView.setNumColumns(1);
+ } else {
+ // And two for landscape
+ mGridView.setNumColumns(2);
+ }
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // keep apollo theme support
+ final Intent apolloThemeIntent = new Intent("com.andrew.apollo.THEMES");
+ apolloThemeIntent.addCategory("android.intent.category.DEFAULT");
+
+ mPackageManager = getActivity().getPackageManager();
+ mThemes = mPackageManager.queryIntentActivities(apolloThemeIntent, 0);
+ mEntries = new String[mThemes.size() + 1];
+ mValues = new String[mThemes.size() + 1];
+ mThemePreview = new Drawable[mThemes.size() + 1];
+
+ // Default items
+ mEntries[0] = getString(R.string.app_name);
+ // mValues[0] = ThemeUtils.ELEVEN_PACKAGE;
+ mThemePreview[0] = getResources().getDrawable(R.drawable.theme_preview);
+
+ for (int i = 0; i < mThemes.size(); i++) {
+ mThemePackageName = mThemes.get(i).activityInfo.packageName.toString();
+ mThemeName = mThemes.get(i).loadLabel(mPackageManager).toString();
+ mEntries[i + 1] = mThemeName;
+ mValues[i + 1] = mThemePackageName;
+
+ // Theme resources
+ try {
+ mThemeResources = mPackageManager.getResourcesForApplication(mThemePackageName
+ .toString());
+ } catch (final NameNotFoundException ignored) {
+ }
+
+ // Theme preview
+ final int previewId = mThemeResources.getIdentifier("theme_preview", "drawable", //$NON-NLS-2$
+ mThemePackageName.toString());
+ if (previewId != 0) {
+ mThemePreview[i + 1] = mThemeResources.getDrawable(previewId);
+ }
+ }
+
+ // Initialize the Adapter
+ mAdapter = new ThemesAdapter(getActivity(), R.layout.fragment_themes_base);
+ // Bind the data
+ mGridView.setAdapter(mAdapter);
+
+ // Get the theme utils
+ mTheme = new ThemeUtils(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ if (info.position > 0) {
+ // Open to the theme's Play Store page
+ menu.add(Menu.NONE, OPEN_IN_PLAY_STORE, Menu.NONE,
+ getString(R.string.context_menu_open_in_play_store));
+ }
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
+ switch (item.getItemId()) {
+ case OPEN_IN_PLAY_STORE:
+ ThemeUtils.openAppPage(getActivity(), mValues[info.position]);
+ return true;
+ default:
+ break;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mTheme.setThemePackageName(mValues[position]);
+ AppMsg.makeText(getActivity(),
+ getString(R.string.theme_set, mEntries[position]), AppMsg.STYLE_CONFIRM)
+ .show();
+ }
+
+ /**
+ * Populates the {@link GridView} with the available themes
+ */
+ private class ThemesAdapter extends ArrayAdapter<ResolveInfo> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource ID of the layout to inflate
+ */
+ private final int mLayoutID;
+
+ /**
+ * Used to cache the theme info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>ThemesAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutID The resource ID of the view to inflate.
+ */
+ public ThemesAdapter(final Context context, final int layoutID) {
+ super(context, 0);
+ // Get the layout ID
+ mLayoutID = layoutID;
+ // Build the cache
+ buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ return mEntries.length;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+
+ /* Recycle ViewHolder's items */
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutID, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set the theme preview
+ holder.mImage.get().setImageDrawable(dataHolder.mPreview);
+ // Set the theme name
+ holder.mLineOne.get().setText(dataHolder.mName);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The
+ * idea is to cache everything before
+ * {@code #getView(int, View, ViewGroup)} is called.
+ */
+ private void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Theme names (line one)
+ mData[i].mName = mEntries[i];
+ // Theme preview
+ mData[i].mPreview = mThemePreview[i];
+ }
+ }
+
+ }
+
+ /**
+ * @param view The {@link View} used to initialize content
+ */
+ public final static class DataHolder {
+
+ public String mName;
+
+ public Drawable mPreview;
+
+ /**
+ * Constructor of <code>DataHolder</code>
+ */
+ public DataHolder() {
+ super();
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/phone/MusicBrowserPhoneFragment.java b/src/com/cyngn/eleven/ui/fragments/phone/MusicBrowserPhoneFragment.java
new file mode 100644
index 0000000..087b462
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/phone/MusicBrowserPhoneFragment.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.phone;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.ViewPager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.PagerAdapter;
+import com.cyngn.eleven.adapters.PagerAdapter.MusicFragments;
+import com.cyngn.eleven.ui.fragments.AlbumFragment;
+import com.cyngn.eleven.ui.fragments.ArtistFragment;
+import com.cyngn.eleven.ui.fragments.SongFragment;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.utils.SortOrder;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+import com.viewpagerindicator.TitlePageIndicator.OnCenterItemClickListener;
+
+/**
+ * This class is used to hold the {@link ViewPager} used for swiping between the
+ * playlists, recent, artists, albums, songs, and genre {@link Fragment}
+ * s for phones.
+ *
+ * @NOTE: The reason the sort orders are taken care of in this fragment rather
+ * than the individual fragments is to keep from showing all of the menu
+ * items on tablet interfaces. That being said, I have a tablet interface
+ * worked out, but I'm going to keep it in the Play Store version of
+ * Apollo for a couple of weeks or so before merging it with CM.
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class MusicBrowserPhoneFragment extends Fragment implements
+ OnCenterItemClickListener {
+
+ /**
+ * Pager
+ */
+ private ViewPager mViewPager;
+
+ /**
+ * VP's adapter
+ */
+ private PagerAdapter mPagerAdapter;
+
+ /**
+ * Theme resources
+ */
+ private ThemeUtils mResources;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public MusicBrowserPhoneFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstance(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(
+ R.layout.fragment_music_browser_phone, container, false);
+
+ // Initialize the adapter
+ mPagerAdapter = new PagerAdapter(getActivity());
+ final MusicFragments[] mFragments = MusicFragments.values();
+ for (final MusicFragments mFragment : mFragments) {
+ mPagerAdapter.add(mFragment.getFragmentClass(), null);
+ }
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)rootView.findViewById(R.id.fragment_home_phone_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen pager loading limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Start on the last page the user was on
+ mViewPager.setCurrentItem(mPreferences.getStartPage());
+
+ // Initialze the TPI
+ final TitlePageIndicator pageIndicator = (TitlePageIndicator)rootView
+ .findViewById(R.id.fragment_home_phone_pager_titles);
+ // Attach the ViewPager
+ pageIndicator.setViewPager(mViewPager);
+ // Scroll to the current artist, album, or song
+ pageIndicator.setOnCenterItemClickListener(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(getActivity());
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Save the last page the use was on
+ mPreferences.setStartPage(mViewPager.getCurrentItem());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPrepareOptionsMenu(final Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ mResources.setFavoriteIcon(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ // Favorite action
+ inflater.inflate(R.menu.favorite, menu);
+ // Shuffle all
+ inflater.inflate(R.menu.shuffle, menu);
+ // Sort orders
+ if (isRecentPage()) {
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isArtistPage()) {
+ inflater.inflate(R.menu.artist_sort_by, menu);
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isAlbumPage()) {
+ inflater.inflate(R.menu.album_sort_by, menu);
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isSongPage()) {
+ inflater.inflate(R.menu.song_sort_by, menu);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_shuffle:
+ // Shuffle all the songs
+ MusicUtils.shuffleAll(getActivity());
+ return true;
+ case R.id.menu_favorite:
+ // Toggle the current track as a favorite and update the menu
+ // item
+ MusicUtils.toggleFavorite();
+ getActivity().invalidateOptionsMenu();
+ return true;
+ case R.id.menu_sort_by_az:
+ if (isArtistPage()) {
+ mPreferences.setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_A_Z);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_A_Z);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_A_Z);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_za:
+ if (isArtistPage()) {
+ mPreferences.setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_Z_A);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_Z_A);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_Z_A);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_artist:
+ if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_ARTIST);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_ARTIST);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_album:
+ if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_ALBUM);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_year:
+ if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_YEAR);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_YEAR);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_duration:
+ if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_DURATION);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_number_of_songs:
+ if (isArtistPage()) {
+ mPreferences
+ .setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS);
+ getAlbumFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_number_of_albums:
+ if (isArtistPage()) {
+ mPreferences
+ .setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS);
+ getArtistFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_filename:
+ if(isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_FILENAME);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_view_as_simple:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("simple");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("simple");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("simple");
+ }
+ NavUtils.goHome(getActivity());
+ return true;
+ case R.id.menu_view_as_detailed:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("detailed");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("detailed");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("detailed");
+ }
+ NavUtils.goHome(getActivity());
+ return true;
+ case R.id.menu_view_as_grid:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("grid");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("grid");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("grid");
+ }
+ NavUtils.goHome(getActivity());
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCenterItemClick(final int position) {
+ // If on the artist fragment, scrolls to the current artist
+ if (position == 2) {
+ getArtistFragment().scrollToCurrentArtist();
+ // If on the album fragment, scrolls to the current album
+ } else if (position == 3) {
+ getAlbumFragment().scrollToCurrentAlbum();
+ // If on the song fragment, scrolls to the current song
+ } else if (position == 4) {
+ getSongFragment().scrollToCurrentSong();
+ }
+ }
+
+ private boolean isArtistPage() {
+ return mViewPager.getCurrentItem() == 2;
+ }
+
+ private ArtistFragment getArtistFragment() {
+ return (ArtistFragment)mPagerAdapter.getFragment(2);
+ }
+
+ private boolean isAlbumPage() {
+ return mViewPager.getCurrentItem() == 3;
+ }
+
+ private AlbumFragment getAlbumFragment() {
+ return (AlbumFragment)mPagerAdapter.getFragment(3);
+ }
+
+ private boolean isSongPage() {
+ return mViewPager.getCurrentItem() == 4;
+ }
+
+ private SongFragment getSongFragment() {
+ return (SongFragment)mPagerAdapter.getFragment(4);
+ }
+
+ private boolean isRecentPage() {
+ return mViewPager.getCurrentItem() == 1;
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/AlbumSongFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/AlbumSongFragment.java
new file mode 100644
index 0000000..64e646c
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/AlbumSongFragment.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.loaders.AlbumSongLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumSongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 11;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public AlbumSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(
+ getActivity(),
+ R.layout.list_item_simple,
+ ProfileSongAdapter.DISPLAY_ALBUM_SETTING
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = AlbumSongLoader.makeAlbumSongCursor(getActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new AlbumSongLoader(getActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/ArtistAlbumFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/ArtistAlbumFragment.java
new file mode 100644
index 0000000..b016e92
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/ArtistAlbumFragment.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ArtistAlbumAdapter;
+import com.cyngn.eleven.loaders.ArtistAlbumLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+import com.cyngn.eleven.widgets.VerticalScrollListener.ScrollableHeader;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the albums from a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAlbumFragment extends Fragment implements LoaderCallbacks<List<Album>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 10;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the grid
+ */
+ private ArtistAlbumAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistAlbumFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ArtistAlbumAdapter(getActivity(),
+ R.layout.list_item_detailed_no_background);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the grid
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Show the songs from the selected album
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(mScrollableHeader,
+ mProfileTabCarousel, 1));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position - 1);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // Delete the album
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mAlbum.mAlbumName, mAlbumList, null).show(
+ getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ mAlbum = mAdapter.getItem(position - 1);
+ NavUtils.openAlbumProfile(getActivity(), mAlbum.mAlbumName,
+ mAlbum.mArtistName, mAlbum.mAlbumId);
+ getActivity().finish();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistAlbumLoader(getActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ // Pause disk cache access to ensure smoother scrolling
+ private final ScrollableHeader mScrollableHeader = new ScrollableHeader() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/ArtistSongFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/ArtistSongFragment.java
new file mode 100644
index 0000000..1aef217
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/ArtistSongFragment.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.loaders.ArtistSongLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistSongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 9;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = ArtistSongLoader.makeArtistSongCursor(getActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistSongLoader(getActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/FavoriteFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/FavoriteFragment.java
new file mode 100644
index 0000000..7eb1dc8
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/FavoriteFragment.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.loaders.FavoritesLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs in {@link FavoritesStore
+ * }.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoriteFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 6;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Artist name used in the context menu
+ */
+ private String mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public FavoriteFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(
+ getActivity(),
+ R.layout.list_item_simple,
+ ProfileSongAdapter.DISPLAY_PLAYLIST_SETTING
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Remove from favorites
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_FAVORITES, Menu.NONE,
+ getString(R.string.remove_from_favorites));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).removeItem(mSelectedId);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = FavoritesLoader.makeFavoritesCursor(getActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new FavoritesLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_favorite));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/GenreSongFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/GenreSongFragment.java
new file mode 100644
index 0000000..1f13c39
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/GenreSongFragment.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.loaders.GenreSongLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreSongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 12;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public GenreSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = GenreSongLoader.makeGenreSongCursor(getActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new GenreSongLoader(getActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/LastAddedFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/LastAddedFragment.java
new file mode 100644
index 0000000..1f60fcd
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/LastAddedFragment.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.loaders.LastAddedLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs the user put on their device
+ * within the last four weeks.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class LastAddedFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 7;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public LastAddedFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(
+ getActivity(),
+ R.layout.list_item_simple,
+ ProfileSongAdapter.DISPLAY_PLAYLIST_SETTING
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = LastAddedLoader.makeLastAddedCursor(getActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new LastAddedLoader(getActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_last_added));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/ui/fragments/profile/PlaylistSongFragment.java b/src/com/cyngn/eleven/ui/fragments/profile/PlaylistSongFragment.java
new file mode 100644
index 0000000..51fc1b6
--- /dev/null
+++ b/src/com/cyngn/eleven/ui/fragments/profile/PlaylistSongFragment.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.dragdrop.DragSortListView;
+import com.cyngn.eleven.dragdrop.DragSortListView.DragScrollProfile;
+import com.cyngn.eleven.dragdrop.DragSortListView.DropListener;
+import com.cyngn.eleven.dragdrop.DragSortListView.RemoveListener;
+import com.cyngn.eleven.loaders.PlaylistSongLoader;
+import com.cyngn.eleven.menu.CreateNewPlaylist;
+import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.widgets.ProfileTabCarousel;
+import com.cyngn.eleven.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistSongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, DropListener, RemoveListener, DragScrollProfile {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 8;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private DragSortListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * The Id of the playlist the songs belong to
+ */
+ private long mPlaylistId;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PlaylistSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(
+ getActivity(),
+ R.layout.edit_track_list_item,
+ ProfileSongAdapter.DISPLAY_PLAYLIST_SETTING
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (DragSortListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // Set the drop listener
+ mListView.setDropListener(this);
+ // Set the swipe to remove listener
+ mListView.setRemoveListener(this);
+ // Quick scroll while dragging
+ mListView.setDragScrollProfile(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ mPlaylistId = arguments.getLong(Config.ID);
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = mSong.mSongId;
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Play next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Remove the song from playlist
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_PLAYLIST, Menu.NONE,
+ getString(R.string.context_menu_remove_from_playlist));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.PLAY_NEXT:
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getActivity()).addSongId(
+ mSelectedId, mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long playlistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getActivity(), new long[] {
+ mSelectedId
+ }, playlistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_PLAYLIST:
+ mAdapter.remove(mSong);
+ mAdapter.notifyDataSetChanged();
+ MusicUtils.removeFromPlaylist(getActivity(), mSong.mSongId, mPlaylistId);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(getActivity(),
+ getArguments().getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new PlaylistSongLoader(getActivity(), mPlaylistId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ if (w > 0.8f) {
+ return mAdapter.getCount() / 0.001f;
+ } else {
+ return 10.0f * w;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remove(final int which) {
+ mSong = mAdapter.getItem(which - 1);
+ mAdapter.remove(mSong);
+ mAdapter.notifyDataSetChanged();
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
+ getActivity().getContentResolver().delete(uri,
+ MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + mSong.mSongId,
+ null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final int from, final int to) {
+ if (from == 0 || to == 0) {
+ mAdapter.notifyDataSetChanged();
+ return;
+ }
+ final int realFrom = from - 1;
+ final int realTo = to - 1;
+ mSong = mAdapter.getItem(realFrom);
+ mAdapter.remove(mSong);
+ mAdapter.insert(mSong, realTo);
+ mAdapter.notifyDataSetChanged();
+ MediaStore.Audio.Playlists.Members.moveItem(getActivity().getContentResolver(),
+ mPlaylistId, realFrom, realTo);
+ }
+}
diff --git a/src/com/cyngn/eleven/utils/ApolloUtils.java b/src/com/cyngn/eleven/utils/ApolloUtils.java
new file mode 100644
index 0000000..1010bad
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/ApolloUtils.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.webkit.WebView;
+import android.widget.Toast;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.ui.activities.ShortcutActivity;
+import com.cyngn.eleven.widgets.ColorPickerView;
+import com.cyngn.eleven.widgets.ColorSchemeDialog;
+import com.devspark.appmsg.AppMsg;
+
+/**
+ * Mostly general and UI helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class ApolloUtils {
+
+ /**
+ * The threshold used calculate if a color is light or dark
+ */
+ private static final int BRIGHTNESS_THRESHOLD = 130;
+
+ /* This class is never initiated */
+ public ApolloUtils() {
+ }
+
+ /**
+ * Used to determine if the device is running Jelly Bean or greater
+ *
+ * @return True if the device is running Jelly Bean or greater, false
+ * otherwise
+ */
+ public static final boolean hasJellyBean() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+ }
+
+ /**
+ * Used to determine if the device is running
+ * Jelly Bean MR2 (Android 4.3) or greater
+ *
+ * @return True if the device is running Jelly Bean MR2 or greater,
+ * false otherwise
+ */
+ public static final boolean hasJellyBeanMR2() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ /**
+ * Used to determine if the device is a tablet or not
+ *
+ * @param context The {@link Context} to use.
+ * @return True if the device is a tablet, false otherwise.
+ */
+ public static final boolean isTablet(final Context context) {
+ final int layout = context.getResources().getConfiguration().screenLayout;
+ return (layout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
+ }
+
+ /**
+ * Used to determine if the device is currently in landscape mode
+ *
+ * @param context The {@link Context} to use.
+ * @return True if the device is in landscape mode, false otherwise.
+ */
+ public static final boolean isLandscape(final Context context) {
+ final int orientation = context.getResources().getConfiguration().orientation;
+ return orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ /**
+ * Execute an {@link AsyncTask} on a thread pool
+ *
+ * @param forceSerial True to force the task to run in serial order
+ * @param task Task to execute
+ * @param args Optional arguments to pass to
+ * {@link AsyncTask#execute(Object[])}
+ * @param <T> Task argument type
+ */
+ @SuppressLint("NewApi")
+ public static <T> void execute(final boolean forceSerial, final AsyncTask<T, ?, ?> task,
+ final T... args) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.DONUT) {
+ throw new UnsupportedOperationException(
+ "This class can only be used on API 4 and newer.");
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || forceSerial) {
+ task.execute(args);
+ } else {
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args);
+ }
+ }
+
+ /**
+ * Used to determine if there is an active data connection and what type of
+ * connection it is if there is one
+ *
+ * @param context The {@link Context} to use
+ * @return True if there is an active data connection, false otherwise.
+ * Also, if the user has checked to only download via Wi-Fi in the
+ * settings, the mobile data and other network connections aren't
+ * returned at all
+ */
+ public static final boolean isOnline(final Context context) {
+ /*
+ * This sort of handles a sudden configuration change, but I think it
+ * should be dealt with in a more professional way.
+ */
+ if (context == null) {
+ return false;
+ }
+
+ boolean state = false;
+ final boolean onlyOnWifi = PreferenceUtils.getInstance(context).onlyOnWifi();
+
+ /* Monitor network connections */
+ final ConnectivityManager connectivityManager = (ConnectivityManager)context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ /* Wi-Fi connection */
+ final NetworkInfo wifiNetwork = connectivityManager
+ .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ if (wifiNetwork != null) {
+ state = wifiNetwork.isConnectedOrConnecting();
+ }
+
+ /* Mobile data connection */
+ final NetworkInfo mbobileNetwork = connectivityManager
+ .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+ if (mbobileNetwork != null) {
+ if (!onlyOnWifi) {
+ state = mbobileNetwork.isConnectedOrConnecting();
+ }
+ }
+
+ /* Other networks */
+ final NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
+ if (activeNetwork != null) {
+ if (!onlyOnWifi) {
+ state = activeNetwork.isConnectedOrConnecting();
+ }
+ }
+
+ return state;
+ }
+
+ /**
+ * Display a {@link Toast} letting the user know what an item does when long
+ * pressed.
+ *
+ * @param view The {@link View} to copy the content description from.
+ */
+ public static void showCheatSheet(final View view) {
+
+ final int[] screenPos = new int[2]; // origin is device display
+ final Rect displayFrame = new Rect(); // includes decorations (e.g.
+ // status bar)
+ view.getLocationOnScreen(screenPos);
+ view.getWindowVisibleDisplayFrame(displayFrame);
+
+ final Context context = view.getContext();
+ final int viewWidth = view.getWidth();
+ final int viewHeight = view.getHeight();
+ final int viewCenterX = screenPos[0] + viewWidth / 2;
+ final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ final int estimatedToastHeight = (int)(48 * context.getResources().getDisplayMetrics().density);
+
+ final Toast cheatSheet = Toast.makeText(context, view.getContentDescription(),
+ Toast.LENGTH_SHORT);
+ final boolean showBelow = screenPos[1] < estimatedToastHeight;
+ if (showBelow) {
+ // Show below
+ // Offsets are after decorations (e.g. status bar) are factored in
+ cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, viewCenterX
+ - screenWidth / 2, screenPos[1] - displayFrame.top + viewHeight);
+ } else {
+ // Show above
+ // Offsets are after decorations (e.g. status bar) are factored in
+ cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, viewCenterX
+ - screenWidth / 2, displayFrame.bottom - screenPos[1]);
+ }
+ cheatSheet.show();
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return An {@link AlertDialog} used to show the open source licenses used
+ * in Apollo.
+ */
+ public static final AlertDialog createOpenSourceDialog(final Context context) {
+ final WebView webView = new WebView(context);
+ webView.loadUrl("file:///android_asset/licenses.html");
+ return new AlertDialog.Builder(context)
+ .setTitle(R.string.settings_open_source_licenses)
+ .setView(webView)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ }
+
+ /**
+ * Calculate whether a color is light or dark, based on a commonly known
+ * brightness formula.
+ *
+ * @see {@literal http://en.wikipedia.org/wiki/HSV_color_space%23Lightness}
+ */
+ public static final boolean isColorDark(final int color) {
+ return (30 * Color.red(color) + 59 * Color.green(color) + 11 * Color.blue(color)) / 100 <= BRIGHTNESS_THRESHOLD;
+ }
+
+ /**
+ * Runs a piece of code after the next layout run
+ *
+ * @param view The {@link View} used.
+ * @param runnable The {@link Runnable} used after the next layout run
+ */
+ @SuppressLint("NewApi")
+ public static void doAfterLayout(final View view, final Runnable runnable) {
+ final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onGlobalLayout() {
+ /* Layout pass done, unregister for further events */
+ if (hasJellyBean()) {
+ view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ } else {
+ view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ }
+ runnable.run();
+ }
+ };
+ view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
+ }
+
+ /**
+ * Creates a new instance of the {@link ImageCache} and {@link ImageFetcher}
+ *
+ * @param activity The {@link Activity} to use.
+ * @return A new {@link ImageFetcher} used to fetch images asynchronously.
+ */
+ public static final ImageFetcher getImageFetcher(final Activity activity) {
+ final ImageFetcher imageFetcher = ImageFetcher.getInstance(activity);
+ imageFetcher.setImageCache(ImageCache.findOrCreateCache(activity));
+ return imageFetcher;
+ }
+
+ /**
+ * Used to create shortcuts for an artist, album, or playlist that is then
+ * placed on the default launcher homescreen
+ *
+ * @param displayName The shortcut name
+ * @param id The ID of the artist, album, playlist, or genre
+ * @param mimeType The MIME type of the shortcut
+ * @param context The {@link Context} to use to
+ */
+ public static void createShortcutIntent(final String displayName, final String artistName,
+ final Long id, final String mimeType, final Activity context) {
+ try {
+ final ImageFetcher fetcher = getImageFetcher(context);
+ Bitmap bitmap = null;
+ if (mimeType.equals(MediaStore.Audio.Albums.CONTENT_TYPE)) {
+ bitmap = fetcher.getCachedBitmap(
+ ImageFetcher.generateAlbumCacheKey(displayName, artistName));
+ } else {
+ bitmap = fetcher.getCachedBitmap(displayName);
+ }
+ if (bitmap == null) {
+ bitmap = BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.default_artwork);
+ }
+
+ // Intent used when the icon is touched
+ final Intent shortcutIntent = new Intent(context, ShortcutActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_VIEW);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ shortcutIntent.putExtra(Config.ID, id);
+ shortcutIntent.putExtra(Config.NAME, displayName);
+ shortcutIntent.putExtra(Config.MIME_TYPE, mimeType);
+
+ // Intent that actually sets the shortcut
+ final Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapUtils.resizeAndCropCenter(bitmap, 96));
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+ intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
+ context.sendBroadcast(intent);
+ AppMsg.makeText(context,
+ context.getString(R.string.pinned_to_home_screen, displayName),
+ AppMsg.STYLE_CONFIRM).show();
+ } catch (final Exception e) {
+ Log.e("ApolloUtils", "createShortcutIntent", e);
+ AppMsg.makeText(
+ context,
+ context.getString(R.string.could_not_be_pinned_to_home_screen, displayName),
+ AppMsg.STYLE_ALERT).show();
+ }
+ }
+
+ /**
+ * Shows the {@link ColorPickerView}
+ *
+ * @param context The {@link Context} to use.
+ */
+ public static void showColorPicker(final Context context) {
+ final ColorSchemeDialog colorPickerView = new ColorSchemeDialog(context);
+ colorPickerView.setButton(AlertDialog.BUTTON_POSITIVE,
+ context.getString(android.R.string.ok), new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ PreferenceUtils.getInstance(context).setDefaultThemeColor(
+ colorPickerView.getColor());
+ }
+ });
+ colorPickerView.setButton(AlertDialog.BUTTON_NEGATIVE,
+ context.getString(R.string.cancel), (OnClickListener) null);
+ colorPickerView.show();
+ }
+
+ /**
+ * Method that removes the support for HardwareAcceleration from a {@link View}.<br/>
+ * <br/>
+ * Check AOSP notice:<br/>
+ * <pre>
+ * 'ComposeShader can only contain shaders of different types (a BitmapShader and a
+ * LinearGradient for instance, but not two instances of BitmapShader)'. But, 'If your
+ * application is affected by any of these missing features or limitations, you can turn
+ * off hardware acceleration for just the affected portion of your application by calling
+ * setLayerType(View.LAYER_TYPE_SOFTWARE, null).'</pre>
+ *
+ * @param v The view
+ */
+ public static void removeHardwareAccelerationSupport(View v) {
+ if (v.getLayerType() != View.LAYER_TYPE_SOFTWARE) {
+ v.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/utils/BitmapUtils.java b/src/com/cyngn/eleven/utils/BitmapUtils.java
new file mode 100644
index 0000000..a26e474
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/BitmapUtils.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+
+/**
+ * {@link Bitmap} specific helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class BitmapUtils {
+
+ /* Initial blur radius. */
+ private static final int DEFAULT_BLUR_RADIUS = 8;
+
+ /** This class is never instantiated */
+ private BitmapUtils() {
+ }
+
+ /**
+ * Takes a bitmap and creates a new slightly blurry version of it.
+ *
+ * @param sentBitmap The {@link Bitmap} to blur.
+ * @return A blurred version of the given {@link Bitmap}.
+ */
+ public static final Bitmap createBlurredBitmap(final Bitmap sentBitmap) {
+ if (sentBitmap == null) {
+ return null;
+ }
+
+ // Stack Blur v1.0 from
+ // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ //
+ // Java Author: Mario Klingemann <mario at quasimondo.com>
+ // http://incubator.quasimondo.com
+ // created Feburary 29, 2004
+ // Android port : Yahel Bouaziz <yahel at kayenko.com>
+ // http://www.kayenko.com
+ // ported april 5th, 2012
+
+ // This is a compromise between Gaussian Blur and Box blur
+ // It creates much better looking blurs than Box Blur, but is
+ // 7x faster than my Gaussian Blur implementation.
+ //
+ // I called it Stack Blur because this describes best how this
+ // filter works internally: it creates a kind of moving stack
+ // of colors whilst scanning through the image. Thereby it
+ // just has to add one new block of color to the right side
+ // of the stack and remove the leftmost color. The remaining
+ // colors on the topmost layer of the stack are either added on
+ // or reduced by one, depending on if they are on the right or
+ // on the left side of the stack.
+ //
+ // If you are using this algorithm in your code please add
+ // the following line:
+ //
+ // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
+
+ final Bitmap mBitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
+
+ final int w = mBitmap.getWidth();
+ final int h = mBitmap.getHeight();
+
+ final int[] pix = new int[w * h];
+ mBitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ final int wm = w - 1;
+ final int hm = h - 1;
+ final int wh = w * h;
+ final int div = DEFAULT_BLUR_RADIUS + DEFAULT_BLUR_RADIUS + 1;
+
+ final int r[] = new int[wh];
+ final int g[] = new int[wh];
+ final int b[] = new int[wh];
+ final int vmin[] = new int[Math.max(w, h)];
+ int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
+
+ int divsum = div + 1 >> 1;
+ divsum *= divsum;
+ final int dv[] = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = i / divsum;
+ }
+
+ yw = yi = 0;
+
+ final int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ final int r1 = DEFAULT_BLUR_RADIUS + 1;
+ int routsum, goutsum, boutsum;
+ int rinsum, ginsum, binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -DEFAULT_BLUR_RADIUS; i <= DEFAULT_BLUR_RADIUS; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + DEFAULT_BLUR_RADIUS];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = p & 0x0000ff;
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = DEFAULT_BLUR_RADIUS;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - DEFAULT_BLUR_RADIUS + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + DEFAULT_BLUR_RADIUS + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = p & 0x0000ff;
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -DEFAULT_BLUR_RADIUS * w;
+ for (i = -DEFAULT_BLUR_RADIUS; i <= DEFAULT_BLUR_RADIUS; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + DEFAULT_BLUR_RADIUS];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = DEFAULT_BLUR_RADIUS;
+ for (y = 0; y < h; y++) {
+ pix[yi] = 0xff000000 | dv[rsum] << 16 | dv[gsum] << 8 | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - DEFAULT_BLUR_RADIUS + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+
+ mBitmap.setPixels(pix, 0, w, 0, 0, w, h);
+ return mBitmap;
+ }
+
+ /**
+ * This is only used when the launcher shortcut is created.
+ *
+ * @param bitmap The artist, album, genre, or playlist image that's going to
+ * be cropped.
+ * @param size The new size.
+ * @return A {@link Bitmap} that has been resized and cropped for a launcher
+ * shortcut.
+ */
+ public static final Bitmap resizeAndCropCenter(final Bitmap bitmap, final int size) {
+ final int w = bitmap.getWidth();
+ final int h = bitmap.getHeight();
+ if (w == size && h == size) {
+ return bitmap;
+ }
+
+ final float mScale = (float)size / Math.min(w, h);
+
+ final Bitmap mTarget = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ final int mWidth = Math.round(mScale * bitmap.getWidth());
+ final int mHeight = Math.round(mScale * bitmap.getHeight());
+ final Canvas mCanvas = new Canvas(mTarget);
+ mCanvas.translate((size - mWidth) / 2f, (size - mHeight) / 2f);
+ mCanvas.scale(mScale, mScale);
+ final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ mCanvas.drawBitmap(bitmap, 0, 0, paint);
+ return mTarget;
+ }
+}
diff --git a/src/com/cyngn/eleven/utils/Lists.java b/src/com/cyngn/eleven/utils/Lists.java
new file mode 100644
index 0000000..97f1503
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/Lists.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012 Google Inc. Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+
+/**
+ * Provides static methods for creating {@code List} instances easily, and other
+ * utility methods for working with lists.
+ */
+public final class Lists {
+
+ /** This class is never instantiated */
+ public Lists() {
+ }
+
+ /**
+ * Creates an empty {@code ArrayList} instance.
+ * <p>
+ * <b>Note:</b> if you only need an <i>immutable</i> empty List, use
+ * {@link Collections#emptyList} instead.
+ *
+ * @return a newly-created, initially-empty {@code ArrayList}
+ */
+ public static final <E> ArrayList<E> newArrayList() {
+ return new ArrayList<E>();
+ }
+
+ /**
+ * Creates an empty {@code LinkedList} instance.
+ * <p>
+ * <b>Note:</b> if you only need an <i>immutable</i> empty List, use
+ * {@link Collections#emptyList} instead.
+ *
+ * @return a newly-created, initially-empty {@code LinkedList}
+ */
+ public static final <E> LinkedList<E> newLinkedList() {
+ return new LinkedList<E>();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/utils/MusicUtils.java b/src/com/cyngn/eleven/utils/MusicUtils.java
new file mode 100644
index 0000000..b466cd4
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/MusicUtils.java
@@ -0,0 +1,1369 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+import android.provider.MediaStore.Audio.ArtistColumns;
+import android.provider.MediaStore.Audio.AudioColumns;
+import android.provider.MediaStore.Audio.Playlists;
+import android.provider.MediaStore.Audio.PlaylistsColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Menu;
+import android.view.SubMenu;
+
+import com.cyngn.eleven.IElevenService;
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.loaders.FavoritesLoader;
+import com.cyngn.eleven.loaders.LastAddedLoader;
+import com.cyngn.eleven.loaders.PlaylistLoader;
+import com.cyngn.eleven.loaders.SongLoader;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.provider.FavoritesStore;
+import com.cyngn.eleven.provider.FavoritesStore.FavoriteColumns;
+import com.cyngn.eleven.provider.RecentStore;
+import com.devspark.appmsg.AppMsg;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.WeakHashMap;
+
+/**
+ * A collection of helpers directly related to music or Apollo's service.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class MusicUtils {
+
+ public static IElevenService mService = null;
+
+ private static int sForegroundActivities = 0;
+
+ private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
+
+ private static final long[] sEmptyList;
+
+ private static ContentValues[] mContentValuesCache = null;
+
+ static {
+ mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
+ sEmptyList = new long[0];
+ }
+
+ /* This class is never initiated */
+ public MusicUtils() {
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param callback The {@link ServiceConnection} to use
+ * @return The new instance of {@link ServiceToken}
+ */
+ public static final ServiceToken bindToService(final Context context,
+ final ServiceConnection callback) {
+ Activity realActivity = ((Activity)context).getParent();
+ if (realActivity == null) {
+ realActivity = (Activity)context;
+ }
+ final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
+ contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
+ final ServiceBinder binder = new ServiceBinder(callback);
+ if (contextWrapper.bindService(
+ new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
+ mConnectionMap.put(contextWrapper, binder);
+ return new ServiceToken(contextWrapper);
+ }
+ return null;
+ }
+
+ /**
+ * @param token The {@link ServiceToken} to unbind from
+ */
+ public static void unbindFromService(final ServiceToken token) {
+ if (token == null) {
+ return;
+ }
+ final ContextWrapper mContextWrapper = token.mWrappedContext;
+ final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
+ if (mBinder == null) {
+ return;
+ }
+ mContextWrapper.unbindService(mBinder);
+ if (mConnectionMap.isEmpty()) {
+ mService = null;
+ }
+ }
+
+ public static final class ServiceBinder implements ServiceConnection {
+ private final ServiceConnection mCallback;
+
+ /**
+ * Constructor of <code>ServiceBinder</code>
+ *
+ * @param context The {@link ServiceConnection} to use
+ */
+ public ServiceBinder(final ServiceConnection callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName className, final IBinder service) {
+ mService = IElevenService.Stub.asInterface(service);
+ if (mCallback != null) {
+ mCallback.onServiceConnected(className, service);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName className) {
+ if (mCallback != null) {
+ mCallback.onServiceDisconnected(className);
+ }
+ mService = null;
+ }
+ }
+
+ public static final class ServiceToken {
+ public ContextWrapper mWrappedContext;
+
+ /**
+ * Constructor of <code>ServiceToken</code>
+ *
+ * @param context The {@link ContextWrapper} to use
+ */
+ public ServiceToken(final ContextWrapper context) {
+ mWrappedContext = context;
+ }
+ }
+
+ /**
+ * Used to make number of labels for the number of artists, albums, songs,
+ * genres, and playlists.
+ *
+ * @param context The {@link Context} to use.
+ * @param pluralInt The ID of the plural string to use.
+ * @param number The number of artists, albums, songs, genres, or playlists.
+ * @return A {@link String} used as a label for the number of artists,
+ * albums, songs, genres, and playlists.
+ */
+ public static final String makeLabel(final Context context, final int pluralInt,
+ final int number) {
+ return context.getResources().getQuantityString(pluralInt, number, number);
+ }
+
+ /**
+ * * Used to create a formatted time string for the duration of tracks.
+ *
+ * @param context The {@link Context} to use.
+ * @param secs The track in seconds.
+ * @return Duration of a track that's properly formatted.
+ */
+ public static final String makeTimeString(final Context context, long secs) {
+ long hours, mins;
+
+ hours = secs / 3600;
+ secs -= hours * 3600;
+ mins = secs / 60;
+ secs -= mins * 60;
+
+ final String durationFormat = context.getResources().getString(
+ hours == 0 ? R.string.durationformatshort : R.string.durationformatlong);
+ return String.format(durationFormat, hours, mins, secs);
+ }
+
+ /**
+ * Changes to the next track
+ */
+ public static void next() {
+ try {
+ if (mService != null) {
+ mService.next();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Changes to the previous track.
+ *
+ * @NOTE The AIDL isn't used here in order to properly use the previous
+ * action. When the user is shuffling, because {@link
+ * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
+ * be able to travel to the previously skipped track. To remedy this,
+ * {@link MusicPlaybackService.#openCurrent()} is called in {@link
+ * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
+ * is called here to specifically invoke the onStartCommand used by
+ * {@link MusicPlaybackService}, which states if the current position
+ * less than 2000 ms, start the track over, otherwise move to the
+ * previously listened track.
+ */
+ public static void previous(final Context context) {
+ final Intent previous = new Intent(context, MusicPlaybackService.class);
+ previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
+ context.startService(previous);
+ }
+
+ /**
+ * Plays or pauses the music.
+ */
+ public static void playOrPause() {
+ try {
+ if (mService != null) {
+ if (mService.isPlaying()) {
+ mService.pause();
+ } else {
+ mService.play();
+ }
+ }
+ } catch (final Exception ignored) {
+ }
+ }
+
+ /**
+ * Cycles through the repeat options.
+ */
+ public static void cycleRepeat() {
+ try {
+ if (mService != null) {
+ switch (mService.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_NONE:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
+ break;
+ case MusicPlaybackService.REPEAT_ALL:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
+ if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ }
+ break;
+ default:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
+ break;
+ }
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Cycles through the shuffle options.
+ */
+ public static void cycleShuffle() {
+ try {
+ if (mService != null) {
+ switch (mService.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NONE:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
+ }
+ break;
+ case MusicPlaybackService.SHUFFLE_NORMAL:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ break;
+ default:
+ break;
+ }
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @return True if we're playing music, false otherwise.
+ */
+ public static final boolean isPlaying() {
+ if (mService != null) {
+ try {
+ return mService.isPlaying();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return The current shuffle mode.
+ */
+ public static final int getShuffleMode() {
+ if (mService != null) {
+ try {
+ return mService.getShuffleMode();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @return The current repeat mode.
+ */
+ public static final int getRepeatMode() {
+ if (mService != null) {
+ try {
+ return mService.getRepeatMode();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @return The current track name.
+ */
+ public static final String getTrackName() {
+ if (mService != null) {
+ try {
+ return mService.getTrackName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current artist name.
+ */
+ public static final String getArtistName() {
+ if (mService != null) {
+ try {
+ return mService.getArtistName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current album name.
+ */
+ public static final String getAlbumName() {
+ if (mService != null) {
+ try {
+ return mService.getAlbumName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current album Id.
+ */
+ public static final long getCurrentAlbumId() {
+ if (mService != null) {
+ try {
+ return mService.getAlbumId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The current song Id.
+ */
+ public static final long getCurrentAudioId() {
+ if (mService != null) {
+ try {
+ return mService.getAudioId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The current artist Id.
+ */
+ public static final long getCurrentArtistId() {
+ if (mService != null) {
+ try {
+ return mService.getArtistId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The audio session Id.
+ */
+ public static final int getAudioSessionId() {
+ if (mService != null) {
+ try {
+ return mService.getAudioSessionId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The queue.
+ */
+ public static final long[] getQueue() {
+ try {
+ if (mService != null) {
+ return mService.getQueue();
+ } else {
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param id The ID of the track to remove.
+ * @return removes track from a playlist or the queue.
+ */
+ public static final int removeTrack(final long id) {
+ try {
+ if (mService != null) {
+ return mService.removeTrack(id);
+ }
+ } catch (final RemoteException ingored) {
+ }
+ return 0;
+ }
+
+ /**
+ * @return The position of the current track in the queue.
+ */
+ public static final int getQueuePosition() {
+ try {
+ if (mService != null) {
+ return mService.getQueuePosition();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return 0;
+ }
+
+ /**
+ * @param cursor The {@link Cursor} used to perform our query.
+ * @return The song list for a MIME type.
+ */
+ public static final long[] getSongListForCursor(Cursor cursor) {
+ if (cursor == null) {
+ return sEmptyList;
+ }
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
+ cursor.moveToFirst();
+ int columnIndex = -1;
+ try {
+ columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ } catch (final IllegalArgumentException notaplaylist) {
+ columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+ }
+ for (int i = 0; i < len; i++) {
+ list[i] = cursor.getLong(columnIndex);
+ cursor.moveToNext();
+ }
+ cursor.close();
+ cursor = null;
+ return list;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The ID of the artist.
+ * @return The song list for an artist.
+ */
+ public static final long[] getSongListForArtist(final Context context, final long id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
+ + AudioColumns.IS_MUSIC + "=1";
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
+ AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The ID of the album.
+ * @return The song list for an album.
+ */
+ public static final long[] getSongListForAlbum(final Context context, final long id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
+ + "=1";
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
+ AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * Plays songs by an artist.
+ *
+ * @param context The {@link Context} to use.
+ * @param artistId The artist Id.
+ * @param position Specify where to start.
+ */
+ public static void playArtist(final Context context, final long artistId, int position) {
+ final long[] artistList = getSongListForArtist(context, artistId);
+ if (artistList != null) {
+ playAll(context, artistList, position, false);
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The ID of the genre.
+ * @return The song list for an genre.
+ */
+ public static final long[] getSongListForGenre(final Context context, final long id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + MediaColumns.TITLE + "!=''");
+ final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
+ Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
+ null, null);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param uri The source of the file
+ */
+ public static void playFile(final Context context, final Uri uri) {
+ if (uri == null || mService == null) {
+ return;
+ }
+
+ // If this is a file:// URI, just use the path directly instead
+ // of going through the open-from-filedescriptor codepath.
+ String filename;
+ String scheme = uri.getScheme();
+ if ("file".equals(scheme)) {
+ filename = uri.getPath();
+ } else {
+ filename = uri.toString();
+ }
+
+ try {
+ mService.stop();
+ mService.openFile(filename);
+ mService.play();
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param list The list of songs to play.
+ * @param position Specify where to start.
+ * @param forceShuffle True to force a shuffle, false otherwise.
+ */
+ public static void playAll(final Context context, final long[] list, int position,
+ final boolean forceShuffle) {
+ if (list.length == 0 || mService == null) {
+ return;
+ }
+ try {
+ if (forceShuffle) {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ } else {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ }
+ final long currentId = mService.getAudioId();
+ final int currentQueuePosition = getQueuePosition();
+ if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
+ final long[] playlist = getQueue();
+ if (Arrays.equals(list, playlist)) {
+ mService.play();
+ return;
+ }
+ }
+ if (position < 0) {
+ position = 0;
+ }
+ mService.open(list, forceShuffle ? -1 : position);
+ mService.play();
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @param list The list to enqueue.
+ */
+ public static void playNext(final long[] list) {
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.enqueue(list, MusicPlaybackService.NEXT);
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ */
+ public static void shuffleAll(final Context context) {
+ Cursor cursor = SongLoader.makeSongCursor(context);
+ final long[] mTrackList = getSongListForCursor(cursor);
+ final int position = 0;
+ if (mTrackList.length == 0 || mService == null) {
+ return;
+ }
+ try {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ final long mCurrentId = mService.getAudioId();
+ final int mCurrentQueuePosition = getQueuePosition();
+ if (position != -1 && mCurrentQueuePosition == position
+ && mCurrentId == mTrackList[position]) {
+ final long[] mPlaylist = getQueue();
+ if (Arrays.equals(mTrackList, mPlaylist)) {
+ mService.play();
+ return;
+ }
+ }
+ mService.open(mTrackList, -1);
+ mService.play();
+ cursor.close();
+ cursor = null;
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Returns The ID for a playlist.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the playlist.
+ * @return The ID for a playlist.
+ */
+ public static final long getIdForPlaylist(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, PlaylistsColumns.NAME + "=?", new String[] {
+ name
+ }, PlaylistsColumns.NAME);
+ int id = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
+ cursor.close();
+ cursor = null;
+ }
+ return id;
+ }
+
+ /**
+ * Returns the Id for an artist.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the artist.
+ * @return The ID for an artist.
+ */
+ public static final long getIdForArtist(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, ArtistColumns.ARTIST + "=?", new String[] {
+ name
+ }, ArtistColumns.ARTIST);
+ int id = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
+ cursor.close();
+ cursor = null;
+ }
+ return id;
+ }
+
+ /**
+ * Returns the ID for an album.
+ *
+ * @param context The {@link Context} to use.
+ * @param albumName The name of the album.
+ * @param artistName The name of the artist
+ * @return The ID for an album.
+ */
+ public static final long getIdForAlbum(final Context context, final String albumName,
+ final String artistName) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, AlbumColumns.ALBUM + "=? AND " + AlbumColumns.ARTIST + "=?", new String[] {
+ albumName, artistName
+ }, AlbumColumns.ALBUM);
+ int id = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
+ cursor.close();
+ cursor = null;
+ }
+ return id;
+ }
+
+ /**
+ * Plays songs from an album.
+ *
+ * @param context The {@link Context} to use.
+ * @param albumId The album Id.
+ * @param position Specify where to start.
+ */
+ public static void playAlbum(final Context context, final long albumId, int position) {
+ final long[] albumList = getSongListForAlbum(context, albumId);
+ if (albumList != null) {
+ playAll(context, albumList, position, false);
+ }
+ }
+
+ /* */
+ public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
+ if (offset + len > ids.length) {
+ len = ids.length - offset;
+ }
+
+ if (mContentValuesCache == null || mContentValuesCache.length != len) {
+ mContentValuesCache = new ContentValues[len];
+ }
+ for (int i = 0; i < len; i++) {
+ if (mContentValuesCache[i] == null) {
+ mContentValuesCache[i] = new ContentValues();
+ }
+ mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
+ mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param name The name of the new playlist.
+ * @return A new playlist ID.
+ */
+ public static final long createPlaylist(final Context context, final String name) {
+ if (name != null && name.length() > 0) {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] projection = new String[] {
+ PlaylistsColumns.NAME
+ };
+ final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
+ Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ projection, selection, null, null);
+ if (cursor.getCount() <= 0) {
+ final ContentValues values = new ContentValues(1);
+ values.put(PlaylistsColumns.NAME, name);
+ final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ values);
+ return Long.parseLong(uri.getLastPathSegment());
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ return -1;
+ }
+ return -1;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param playlistId The playlist ID.
+ */
+ public static void clearPlaylist(final Context context, final int playlistId) {
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
+ context.getContentResolver().delete(uri, null, null);
+ return;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param ids The id of the song(s) to add.
+ * @param playlistid The id of the playlist being added to.
+ */
+ public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
+ final int size = ids.length;
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] projection = new String[] {
+ "count(*)"
+ };
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
+ Cursor cursor = resolver.query(uri, projection, null, null, null);
+ cursor.moveToFirst();
+ final int base = cursor.getInt(0);
+ cursor.close();
+ cursor = null;
+ int numinserted = 0;
+ for (int offSet = 0; offSet < size; offSet += 1000) {
+ makeInsertItems(ids, offSet, 1000, base);
+ numinserted += resolver.bulkInsert(uri, mContentValuesCache);
+ }
+ final String message = context.getResources().getQuantityString(
+ R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
+ AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
+ }
+
+ /**
+ * Removes a single track from a given playlist
+ * @param context The {@link Context} to use.
+ * @param id The id of the song to remove.
+ * @param playlistId The id of the playlist being removed from.
+ */
+ public static void removeFromPlaylist(final Context context, final long id,
+ final long playlistId) {
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
+ final ContentResolver resolver = context.getContentResolver();
+ resolver.delete(uri, Playlists.Members.AUDIO_ID + " = ? ", new String[] {
+ Long.toString(id)
+ });
+ final String message = context.getResources().getQuantityString(
+ R.plurals.NNNtracksfromplaylist, 1, 1);
+ AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param list The list to enqueue.
+ */
+ public static void addToQueue(final Context context, final long[] list) {
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.enqueue(list, MusicPlaybackService.LAST);
+ final String message = makeLabel(context, R.plurals.NNNtrackstoqueue, list.length);
+ AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param id The song ID.
+ */
+ public static void setRingtone(final Context context, final long id) {
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
+ try {
+ final ContentValues values = new ContentValues(2);
+ values.put(AudioColumns.IS_RINGTONE, "1");
+ values.put(AudioColumns.IS_ALARM, "1");
+ resolver.update(uri, values, null, null);
+ } catch (final UnsupportedOperationException ingored) {
+ return;
+ }
+
+ final String[] projection = new String[] {
+ BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
+ };
+
+ final String selection = BaseColumns._ID + "=" + id;
+ Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
+ selection, null, null);
+ try {
+ if (cursor != null && cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
+ final String message = context.getString(R.string.set_as_ringtone,
+ cursor.getString(2));
+ AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The id of the album.
+ * @return The song count for an album.
+ */
+ public static final String getSongCountForAlbum(final Context context, final long id) {
+ if (id == -1) {
+ return null;
+ }
+ Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
+ Cursor cursor = context.getContentResolver().query(uri, new String[] {
+ AlbumColumns.NUMBER_OF_SONGS
+ }, null, null, null);
+ String songCount = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ songCount = cursor.getString(0);
+ }
+ cursor.close();
+ cursor = null;
+ }
+ return songCount;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The id of the album.
+ * @return The release date for an album.
+ */
+ public static final String getReleaseDateForAlbum(final Context context, final long id) {
+ if (id == -1) {
+ return null;
+ }
+ Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, id);
+ Cursor cursor = context.getContentResolver().query(uri, new String[] {
+ AlbumColumns.FIRST_YEAR
+ }, null, null, null);
+ String releaseDate = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ releaseDate = cursor.getString(0);
+ }
+ cursor.close();
+ cursor = null;
+ }
+ return releaseDate;
+ }
+
+ /**
+ * @return The path to the currently playing file as {@link String}
+ */
+ public static final String getFilePath() {
+ try {
+ if (mService != null) {
+ return mService.getPath();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * @param from The index the item is currently at.
+ * @param to The index the item is moving to.
+ */
+ public static void moveQueueItem(final int from, final int to) {
+ try {
+ if (mService != null) {
+ mService.moveQueueItem(from, to);
+ } else {
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Toggles the current song as a favorite.
+ */
+ public static void toggleFavorite() {
+ try {
+ if (mService != null) {
+ mService.toggleFavorite();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @return True if the current song is a favorite, false otherwise.
+ */
+ public static final boolean isFavorite() {
+ try {
+ if (mService != null) {
+ return mService.isFavorite();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return false;
+ }
+
+ /**
+ * @param context The {@link Context} to sue
+ * @param playlistId The playlist Id
+ * @return The track list for a playlist
+ */
+ public static final long[] getSongListForPlaylist(final Context context, final long playlistId) {
+ final String[] projection = new String[] {
+ MediaStore.Audio.Playlists.Members.AUDIO_ID
+ };
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(playlistId)), projection, null, null,
+ MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+
+ if (cursor != null) {
+ final long[] list = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * Plays a user created playlist.
+ *
+ * @param context The {@link Context} to use.
+ * @param playlistId The playlist Id.
+ */
+ public static void playPlaylist(final Context context, final long playlistId) {
+ final long[] playlistList = getSongListForPlaylist(context, playlistId);
+ if (playlistList != null) {
+ playAll(context, playlistList, -1, false);
+ }
+ }
+
+ /**
+ * @param cursor The {@link Cursor} used to gather the list in our favorites
+ * database
+ * @return The song list for the favorite playlist
+ */
+ public final static long[] getSongListForFavoritesCursor(Cursor cursor) {
+ if (cursor == null) {
+ return sEmptyList;
+ }
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
+ cursor.moveToFirst();
+ int colidx = -1;
+ try {
+ colidx = cursor.getColumnIndexOrThrow(FavoriteColumns.ID);
+ } catch (final Exception ignored) {
+ }
+ for (int i = 0; i < len; i++) {
+ list[i] = cursor.getLong(colidx);
+ cursor.moveToNext();
+ }
+ cursor.close();
+ cursor = null;
+ return list;
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return The song list from our favorites database
+ */
+ public final static long[] getSongListForFavorites(final Context context) {
+ Cursor cursor = FavoritesLoader.makeFavoritesCursor(context);
+ if (cursor != null) {
+ final long[] list = getSongListForFavoritesCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * Play the songs that have been marked as favorites.
+ *
+ * @param context The {@link Context} to use
+ */
+ public static void playFavorites(final Context context) {
+ playAll(context, getSongListForFavorites(context), 0, false);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return The song list for the last added playlist
+ */
+ public static final long[] getSongListForLastAdded(final Context context) {
+ final Cursor cursor = LastAddedLoader.makeLastAddedCursor(context);
+ if (cursor != null) {
+ final int count = cursor.getCount();
+ final long[] list = new long[count];
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getLong(0);
+ }
+ return list;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * Plays the last added songs from the past two weeks.
+ *
+ * @param context The {@link Context} to use
+ */
+ public static void playLastAdded(final Context context) {
+ playAll(context, getSongListForLastAdded(context), 0, false);
+ }
+
+ /**
+ * Creates a sub menu used to add items to a new playlist or an existsing
+ * one.
+ *
+ * @param context The {@link Context} to use.
+ * @param groupId The group Id of the menu.
+ * @param subMenu The {@link SubMenu} to add to.
+ * @param showFavorites True if we should show the option to add to the
+ * Favorites cache.
+ */
+ public static void makePlaylistMenu(final Context context, final int groupId,
+ final SubMenu subMenu, final boolean showFavorites) {
+ subMenu.clear();
+ if (showFavorites) {
+ subMenu.add(groupId, FragmentMenuItems.ADD_TO_FAVORITES, Menu.NONE,
+ R.string.add_to_favorites);
+ }
+ subMenu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
+ Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
+ if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
+ while (!cursor.isAfterLast()) {
+ final Intent intent = new Intent();
+ String name = cursor.getString(1);
+ if (name != null) {
+ intent.putExtra("playlist", getIdForPlaylist(context, name));
+ subMenu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
+ name).setIntent(intent);
+ }
+ cursor.moveToNext();
+ }
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+
+ /**
+ * Called when one of the lists should refresh or requery.
+ */
+ public static void refresh() {
+ try {
+ if (mService != null) {
+ mService.refresh();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Queries {@link RecentStore} for the last album played by an artist
+ *
+ * @param context The {@link Context} to use
+ * @param artistName The artist name
+ * @return The last album name played by an artist
+ */
+ public static final String getLastAlbumForArtist(final Context context, final String artistName) {
+ return RecentStore.getInstance(context).getAlbumName(artistName);
+ }
+
+ /**
+ * Seeks the current track to a desired position
+ *
+ * @param position The position to seek to
+ */
+ public static void seek(final long position) {
+ if (mService != null) {
+ try {
+ mService.seek(position);
+ } catch (final RemoteException ignored) {
+ }
+ }
+ }
+
+ /**
+ * @return The current position time of the track
+ */
+ public static final long position() {
+ if (mService != null) {
+ try {
+ return mService.position();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @return The total length of the current track
+ */
+ public static final long duration() {
+ if (mService != null) {
+ try {
+ return mService.duration();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @param position The position to move the queue to
+ */
+ public static void setQueuePosition(final int position) {
+ if (mService != null) {
+ try {
+ mService.setQueuePosition(position);
+ } catch (final RemoteException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Clears the qeueue
+ */
+ public static void clearQueue() {
+ try {
+ mService.removeTracks(0, Integer.MAX_VALUE);
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * Used to build and show a notification when Apollo is sent into the
+ * background
+ *
+ * @param context The {@link Context} to use.
+ */
+ public static void notifyForegroundStateChanged(final Context context, boolean inForeground) {
+ int old = sForegroundActivities;
+ if (inForeground) {
+ sForegroundActivities++;
+ } else {
+ sForegroundActivities--;
+ }
+
+ if (old == 0 || sForegroundActivities == 0) {
+ final Intent intent = new Intent(context, MusicPlaybackService.class);
+ intent.setAction(MusicPlaybackService.FOREGROUND_STATE_CHANGED);
+ intent.putExtra(MusicPlaybackService.NOW_IN_FOREGROUND, sForegroundActivities != 0);
+ context.startService(intent);
+ }
+ }
+
+ /**
+ * Perminately deletes item(s) from the user's device
+ *
+ * @param context The {@link Context} to use.
+ * @param list The item(s) to delete.
+ */
+ public static void deleteTracks(final Context context, final long[] list) {
+ final String[] projection = new String[] {
+ BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
+ };
+ final StringBuilder selection = new StringBuilder();
+ selection.append(BaseColumns._ID + " IN (");
+ for (int i = 0; i < list.length; i++) {
+ selection.append(list[i]);
+ if (i < list.length - 1) {
+ selection.append(",");
+ }
+ }
+ selection.append(")");
+ final Cursor c = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
+ null, null);
+ if (c != null) {
+ // Step 1: Remove selected tracks from the current playlist, as well
+ // as from the album art cache
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ // Remove from current playlist
+ final long id = c.getLong(0);
+ removeTrack(id);
+ // Remove from the favorites playlist
+ FavoritesStore.getInstance(context).removeItem(id);
+ // Remove any items in the recents database
+ RecentStore.getInstance(context).removeItem(c.getLong(2));
+ c.moveToNext();
+ }
+
+ // Step 2: Remove selected tracks from the database
+ context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ selection.toString(), null);
+
+ // Step 3: Remove files from card
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ final String name = c.getString(1);
+ final File f = new File(name);
+ try { // File.delete can throw a security exception
+ if (!f.delete()) {
+ // I'm not sure if we'd ever get here (deletion would
+ // have to fail, but no exception thrown)
+ Log.e("MusicUtils", "Failed to delete file " + name);
+ }
+ c.moveToNext();
+ } catch (final SecurityException ex) {
+ c.moveToNext();
+ }
+ }
+ c.close();
+ }
+
+ final String message = makeLabel(context, R.plurals.NNNtracksdeleted, list.length);
+
+ AppMsg.makeText((Activity)context, message, AppMsg.STYLE_CONFIRM).show();
+ // We deleted a number of tracks, which could affect any number of
+ // things
+ // in the media content domain, so update everything.
+ context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
+ // Notify the lists to update
+ refresh();
+ }
+}
diff --git a/src/com/cyngn/eleven/utils/NavUtils.java b/src/com/cyngn/eleven/utils/NavUtils.java
new file mode 100644
index 0000000..852ce09
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/NavUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.media.audiofx.AudioEffect;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.ui.activities.AudioPlayerActivity;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.ui.activities.SearchActivity;
+import com.cyngn.eleven.ui.activities.SettingsActivity;
+import com.devspark.appmsg.AppMsg;
+
+/**
+ * Various navigation helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class NavUtils {
+
+ /**
+ * Opens the profile of an artist.
+ *
+ * @param context The {@link Activity} to use.
+ * @param artistName The name of the artist
+ */
+ public static void openArtistProfile(final Activity context,
+ final String artistName) {
+
+ // Create a new bundle to transfer the artist info
+ final Bundle bundle = new Bundle();
+ bundle.putLong(Config.ID, MusicUtils.getIdForArtist(context, artistName));
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Artists.CONTENT_TYPE);
+ bundle.putString(Config.ARTIST_NAME, artistName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(context, ProfileActivity.class);
+ intent.putExtras(bundle);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Opens the profile of an album.
+ *
+ * @param context The {@link Activity} to use.
+ * @param albumName The name of the album
+ * @param artistName The name of the album artist
+ * @param albumId The id of the album
+ */
+ public static void openAlbumProfile(final Activity context,
+ final String albumName, final String artistName, final long albumId) {
+
+ // Create a new bundle to transfer the album info
+ final Bundle bundle = new Bundle();
+ bundle.putString(Config.ALBUM_YEAR, MusicUtils.getReleaseDateForAlbum(context, albumId));
+ bundle.putString(Config.ARTIST_NAME, artistName);
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ bundle.putLong(Config.ID, albumId);
+ bundle.putString(Config.NAME, albumName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(context, ProfileActivity.class);
+ intent.putExtras(bundle);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Opens the sound effects panel or DSP manager in CM
+ *
+ * @param context The {@link Activity} to use.
+ */
+ public static void openEffectsPanel(final Activity context) {
+ try {
+ final Intent effects = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
+ effects.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, MusicUtils.getAudioSessionId());
+ context.startActivity(effects);
+ } catch (final ActivityNotFoundException notFound) {
+ AppMsg.makeText(context, context.getString(R.string.no_effects_for_you),
+ AppMsg.STYLE_ALERT);
+ }
+ }
+
+ /**
+ * Opens to {@link SettingsActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ */
+ public static void openSettings(final Activity activity) {
+ final Intent intent = new Intent(activity, SettingsActivity.class);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Opens to {@link AudioPlayerActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ */
+ public static void openAudioPlayer(final Activity activity) {
+ final Intent intent = new Intent(activity, AudioPlayerActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ activity.finish();
+ }
+
+ /**
+ * Opens to {@link SearchActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ * @param query The search query.
+ */
+ public static void openSearch(final Activity activity, final String query) {
+ final Bundle bundle = new Bundle();
+ final Intent intent = new Intent(activity, SearchActivity.class);
+ intent.putExtra(SearchManager.QUERY, query);
+ intent.putExtras(bundle);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Opens to {@link HomeActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ */
+ public static void goHome(final Activity activity) {
+ final Intent intent = new Intent(activity, HomeActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ activity.finish();
+ }
+}
diff --git a/src/com/cyngn/eleven/utils/PreferenceUtils.java b/src/com/cyngn/eleven/utils/PreferenceUtils.java
new file mode 100644
index 0000000..27cadc6
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/PreferenceUtils.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.fragments.AlbumFragment;
+import com.cyngn.eleven.ui.fragments.ArtistFragment;
+import com.cyngn.eleven.ui.fragments.SongFragment;
+import com.cyngn.eleven.ui.fragments.phone.MusicBrowserPhoneFragment;
+import com.cyngn.eleven.ui.fragments.profile.AlbumSongFragment;
+import com.cyngn.eleven.ui.fragments.profile.ArtistAlbumFragment;
+import com.cyngn.eleven.ui.fragments.profile.ArtistSongFragment;
+
+/**
+ * A collection of helpers designed to get and set various preferences across
+ * Apollo.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class PreferenceUtils {
+
+ /* Default start page (Artist page) */
+ public static final int DEFFAULT_PAGE = 2;
+
+ /* Saves the last page the pager was on in {@link MusicBrowserPhoneFragment} */
+ public static final String START_PAGE = "start_page";
+
+ // Sort order for the artist list
+ public static final String ARTIST_SORT_ORDER = "artist_sort_order";
+
+ // Sort order for the artist song list
+ public static final String ARTIST_SONG_SORT_ORDER = "artist_song_sort_order";
+
+ // Sort order for the artist album list
+ public static final String ARTIST_ALBUM_SORT_ORDER = "artist_album_sort_order";
+
+ // Sort order for the album list
+ public static final String ALBUM_SORT_ORDER = "album_sort_order";
+
+ // Sort order for the album song list
+ public static final String ALBUM_SONG_SORT_ORDER = "album_song_sort_order";
+
+ // Sort order for the song list
+ public static final String SONG_SORT_ORDER = "song_sort_order";
+
+ // Sets the type of layout to use for the artist list
+ public static final String ARTIST_LAYOUT = "artist_layout";
+
+ // Sets the type of layout to use for the album list
+ public static final String ALBUM_LAYOUT = "album_layout";
+
+ // Sets the type of layout to use for the recent list
+ public static final String RECENT_LAYOUT = "recent_layout";
+
+ // Key used to download images only on Wi-Fi
+ public static final String ONLY_ON_WIFI = "only_on_wifi";
+
+ // Key that gives permissions to download missing album covers
+ public static final String DOWNLOAD_MISSING_ARTWORK = "download_missing_artwork";
+
+ // Key that gives permissions to download missing artist images
+ public static final String DOWNLOAD_MISSING_ARTIST_IMAGES = "download_missing_artist_images";
+
+ // Key used to set the overall theme color
+ public static final String DEFAULT_THEME_COLOR = "default_theme_color";
+
+ private static PreferenceUtils sInstance;
+
+ private final SharedPreferences mPreferences;
+
+ /**
+ * Constructor for <code>PreferenceUtils</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public PreferenceUtils(final Context context) {
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return A singleton of this class
+ */
+ public static final PreferenceUtils getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new PreferenceUtils(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Saves the current page the user is on when they close the app.
+ *
+ * @param value The last page the pager was on when the onDestroy is called
+ * in {@link MusicBrowserPhoneFragment}.
+ */
+ public void setStartPage(final int value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putInt(START_PAGE, value);
+ editor.apply();
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Returns the last page the user was on when the app was exited.
+ *
+ * @return The page to start on when the app is opened.
+ */
+ public final int getStartPage() {
+ return mPreferences.getInt(START_PAGE, DEFFAULT_PAGE);
+ }
+
+ /**
+ * Sets the new theme color.
+ *
+ * @param value The new theme color to use.
+ */
+ public void setDefaultThemeColor(final int value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putInt(DEFAULT_THEME_COLOR, value);
+ editor.apply();
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Returns the current theme color.
+ *
+ * @param context The {@link Context} to use.
+ * @return The default theme color.
+ */
+ public final int getDefaultThemeColor(final Context context) {
+ return mPreferences.getInt(DEFAULT_THEME_COLOR,
+ context.getResources().getColor(R.color.holo_blue_light));
+ }
+
+ /**
+ * @return True if the user has checked to only download images on Wi-Fi,
+ * false otherwise
+ */
+ public final boolean onlyOnWifi() {
+ return mPreferences.getBoolean(ONLY_ON_WIFI, true);
+ }
+
+ /**
+ * @return True if the user has checked to download missing album covers,
+ * false otherwise.
+ */
+ public final boolean downloadMissingArtwork() {
+ return mPreferences.getBoolean(DOWNLOAD_MISSING_ARTWORK, true);
+ }
+
+ /**
+ * @return True if the user has checked to download missing artist images,
+ * false otherwise.
+ */
+ public final boolean downloadMissingArtistImages() {
+ return mPreferences.getBoolean(DOWNLOAD_MISSING_ARTIST_IMAGES, true);
+ }
+
+ /**
+ * Saves the sort order for a list.
+ *
+ * @param key Which sort order to change
+ * @param value The new sort order
+ */
+ private void setSortOrder(final String key, final String value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(key, value);
+ editor.apply();
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Sets the sort order for the artist list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistSortOrder(final String value) {
+ setSortOrder(ARTIST_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist list in {@link ArtistFragment}
+ */
+ public final String getArtistSortOrder() {
+ return mPreferences.getString(ARTIST_SORT_ORDER, SortOrder.ArtistSortOrder.ARTIST_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the artist song list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistSongSortOrder(final String value) {
+ setSortOrder(ARTIST_SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist song list in
+ * {@link ArtistSongFragment}
+ */
+ public final String getArtistSongSortOrder() {
+ return mPreferences.getString(ARTIST_SONG_SORT_ORDER,
+ SortOrder.ArtistSongSortOrder.SONG_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the artist album list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistAlbumSortOrder(final String value) {
+ setSortOrder(ARTIST_ALBUM_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist album list in
+ * {@link ArtistAlbumFragment}
+ */
+ public final String getArtistAlbumSortOrder() {
+ return mPreferences.getString(ARTIST_ALBUM_SORT_ORDER,
+ SortOrder.ArtistAlbumSortOrder.ALBUM_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the album list.
+ *
+ * @param value The new sort order
+ */
+ public void setAlbumSortOrder(final String value) {
+ setSortOrder(ALBUM_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the album list in {@link AlbumFragment}
+ */
+ public final String getAlbumSortOrder() {
+ return mPreferences.getString(ALBUM_SORT_ORDER, SortOrder.AlbumSortOrder.ALBUM_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the album song list.
+ *
+ * @param value The new sort order
+ */
+ public void setAlbumSongSortOrder(final String value) {
+ setSortOrder(ALBUM_SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the album song in
+ * {@link AlbumSongFragment}
+ */
+ public final String getAlbumSongSortOrder() {
+ return mPreferences.getString(ALBUM_SONG_SORT_ORDER,
+ SortOrder.AlbumSongSortOrder.SONG_TRACK_LIST);
+ }
+
+ /**
+ * Sets the sort order for the song list.
+ *
+ * @param value The new sort order
+ */
+ public void setSongSortOrder(final String value) {
+ setSortOrder(SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the song list in {@link SongFragment}
+ */
+ public final String getSongSortOrder() {
+ return mPreferences.getString(SONG_SORT_ORDER, SortOrder.SongSortOrder.SONG_A_Z);
+ }
+
+ /**
+ * Saves the layout type for a list
+ *
+ * @param key Which layout to change
+ * @param value The new layout type
+ */
+ private void setLayoutType(final String key, final String value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(key, value);
+ editor.apply();
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Sets the layout type for the artist list
+ *
+ * @param value The new layout type
+ */
+ public void setArtistLayout(final String value) {
+ setLayoutType(ARTIST_LAYOUT, value);
+ }
+
+ /**
+ * Sets the layout type for the album list
+ *
+ * @param value The new layout type
+ */
+ public void setAlbumLayout(final String value) {
+ setLayoutType(ALBUM_LAYOUT, value);
+ }
+
+ /**
+ * Sets the layout type for the recent list
+ *
+ * @param value The new layout type
+ */
+ public void setRecentLayout(final String value) {
+ setLayoutType(RECENT_LAYOUT, value);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isSimpleLayout(final String which, final Context context) {
+ final String simple = "simple";
+ final String defaultValue = "grid";
+ return mPreferences.getString(which, defaultValue).equals(simple);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isDetailedLayout(final String which, final Context context) {
+ final String detailed = "detailed";
+ final String defaultValue = "grid";
+ return mPreferences.getString(which, defaultValue).equals(detailed);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isGridLayout(final String which, final Context context) {
+ final String grid = "grid";
+ final String defaultValue = "simple";
+ return mPreferences.getString(which, defaultValue).equals(grid);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/utils/SortOrder.java b/src/com/cyngn/eleven/utils/SortOrder.java
new file mode 100644
index 0000000..a71bce5
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/SortOrder.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.provider.MediaStore;
+
+/**
+ * Holds all of the sort orders for each list type.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class SortOrder {
+
+ /** This class is never instantiated */
+ public SortOrder() {
+ }
+
+ /**
+ * Artist sort order entries.
+ */
+ public static interface ArtistSortOrder {
+ /* Artist sort order A-Z */
+ public final static String ARTIST_A_Z = MediaStore.Audio.Artists.DEFAULT_SORT_ORDER;
+
+ /* Artist sort order Z-A */
+ public final static String ARTIST_Z_A = ARTIST_A_Z + " DESC";
+
+ /* Artist sort order number of songs */
+ public final static String ARTIST_NUMBER_OF_SONGS = MediaStore.Audio.Artists.NUMBER_OF_TRACKS
+ + " DESC";
+
+ /* Artist sort order number of albums */
+ public final static String ARTIST_NUMBER_OF_ALBUMS = MediaStore.Audio.Artists.NUMBER_OF_ALBUMS
+ + " DESC";
+ }
+
+ /**
+ * Album sort order entries.
+ */
+ public static interface AlbumSortOrder {
+ /* Album sort order A-Z */
+ public final static String ALBUM_A_Z = MediaStore.Audio.Albums.DEFAULT_SORT_ORDER;
+
+ /* Album sort order Z-A */
+ public final static String ALBUM_Z_A = ALBUM_A_Z + " DESC";
+
+ /* Album sort order songs */
+ public final static String ALBUM_NUMBER_OF_SONGS = MediaStore.Audio.Albums.NUMBER_OF_SONGS
+ + " DESC";
+
+ /* Album sort order artist */
+ public final static String ALBUM_ARTIST = MediaStore.Audio.Albums.ARTIST;
+
+ /* Album sort order year */
+ public final static String ALBUM_YEAR = MediaStore.Audio.Albums.FIRST_YEAR + " DESC";
+
+ }
+
+ /**
+ * Song sort order entries.
+ */
+ public static interface SongSortOrder {
+ /* Song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Song sort order artist */
+ public final static String SONG_ARTIST = MediaStore.Audio.Media.ARTIST;
+
+ /* Song sort order album */
+ public final static String SONG_ALBUM = MediaStore.Audio.Media.ALBUM;
+
+ /* Song sort order year */
+ public final static String SONG_YEAR = MediaStore.Audio.Media.YEAR + " DESC";
+
+ /* Song sort order duration */
+ public final static String SONG_DURATION = MediaStore.Audio.Media.DURATION + " DESC";
+
+ /* Song sort order date */
+ public final static String SONG_DATE = MediaStore.Audio.Media.DATE_ADDED + " DESC";
+
+ /* Song sort order filename */
+ public final static String SONG_FILENAME = MediaStore.Audio.Media.DATA;
+ }
+
+ /**
+ * Album song sort order entries.
+ */
+ public static interface AlbumSongSortOrder {
+ /* Album song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Album song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Album song sort order track list */
+ public final static String SONG_TRACK_LIST = MediaStore.Audio.Media.TRACK + ", "
+ + MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Album song sort order duration */
+ public final static String SONG_DURATION = SongSortOrder.SONG_DURATION;
+
+ /* Album song sort order filename */
+ public final static String SONG_FILENAME = SongSortOrder.SONG_FILENAME;
+ }
+
+ /**
+ * Artist song sort order entries.
+ */
+ public static interface ArtistSongSortOrder {
+ /* Artist song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Artist song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Artist song sort order album */
+ public final static String SONG_ALBUM = MediaStore.Audio.Media.ALBUM;
+
+ /* Artist song sort order year */
+ public final static String SONG_YEAR = MediaStore.Audio.Media.YEAR + " DESC";
+
+ /* Artist song sort order duration */
+ public final static String SONG_DURATION = MediaStore.Audio.Media.DURATION + " DESC";
+
+ /* Artist song sort order date */
+ public final static String SONG_DATE = MediaStore.Audio.Media.DATE_ADDED + " DESC";
+
+ /* Artist song sort order filename */
+ public final static String SONG_FILENAME = SongSortOrder.SONG_FILENAME;
+ }
+
+ /**
+ * Artist album sort order entries.
+ */
+ public static interface ArtistAlbumSortOrder {
+ /* Artist album sort order A-Z */
+ public final static String ALBUM_A_Z = MediaStore.Audio.Albums.DEFAULT_SORT_ORDER;
+
+ /* Artist album sort order Z-A */
+ public final static String ALBUM_Z_A = ALBUM_A_Z + " DESC";
+
+ /* Artist album sort order songs */
+ public final static String ALBUM_NUMBER_OF_SONGS = MediaStore.Audio.Artists.Albums.NUMBER_OF_SONGS
+ + " DESC";
+
+ /* Artist album sort order year */
+ public final static String ALBUM_YEAR = MediaStore.Audio.Artists.Albums.FIRST_YEAR
+ + " DESC";
+ }
+
+}
diff --git a/src/com/cyngn/eleven/utils/ThemeUtils.java b/src/com/cyngn/eleven/utils/ThemeUtils.java
new file mode 100644
index 0000000..ab19fa3
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/ThemeUtils.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+
+/**
+ * In order to implement the theme chooser for Apollo, this class returns a
+ * {@link Resources} object that can be used like normal. In other words, when
+ * {@code getDrawable()} or {@code getColor()} is called, the object returned is
+ * from the current theme package name and because all of the theme resource
+ * identifiers are the same as all of Apollo's resources a little less code is
+ * used to implement the theme chooser.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeUtils {
+
+ /**
+ * Used to searc the "Apps" section of the Play Store for "Apollo Themes".
+ */
+ private static final String SEARCH_URI = "https://market.android.com/search?q=%s&c=apps&featured=APP_STORE_SEARCH";
+
+ /**
+ * Used to search the Play Store for a specific theme.
+ */
+ private static final String APP_URI = "market://details?id=";
+
+ /**
+ * Default package name.
+ */
+ public static final String ELEVEN_PACKAGE = "com.cyngn.eleven";
+
+ /**
+ * Current theme package name.
+ */
+ public static final String PACKAGE_NAME = "theme_package_name";
+
+ /**
+ * Used to get and set the theme package name.
+ */
+ private final SharedPreferences mPreferences;
+
+ /**
+ * The theme package name.
+ */
+ private final String mThemePackage;
+
+ /**
+ * The keyword to use when search for different themes.
+ */
+ private static String sApolloSearch;
+
+ /**
+ * This is the current theme color as set by the color picker.
+ */
+ private final int mCurrentThemeColor;
+
+ /**
+ * Package manager
+ */
+ private final PackageManager mPackageManager;
+
+ /**
+ * Custom action bar layout
+ */
+ private final View mActionBarLayout;
+
+ /**
+ * The theme resources.
+ */
+ private Resources mResources;
+
+ /**
+ * Constructor for <code>ThemeUtils</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public ThemeUtils(final Context context) {
+ // Get the search query
+ sApolloSearch = context.getString(R.string.apollo_themes_shop_key);
+ // Get the preferences
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ // Get the theme package name
+ mThemePackage = getThemePackageName();
+ // Initialze the package manager
+ mPackageManager = context.getPackageManager();
+ try {
+ // Find the theme resources
+ mResources = mPackageManager.getResourcesForApplication(mThemePackage);
+ } catch (final Exception e) {
+ // If the user isn't using a theme, then the resources should be
+ // Eleven's.
+ setThemePackageName(ELEVEN_PACKAGE);
+ }
+ // Get the current theme color
+ mCurrentThemeColor = PreferenceUtils.getInstance(context).getDefaultThemeColor(context);
+ // Inflate the custom layout
+ mActionBarLayout = LayoutInflater.from(context).inflate(R.layout.action_bar, null);
+ }
+
+ /**
+ * Set the new theme package name.
+ *
+ * @param packageName The package name of the theme to be set.
+ */
+ public void setThemePackageName(final String packageName) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(PACKAGE_NAME, packageName);
+ editor.apply();
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Return the current theme package name.
+ *
+ * @return The default theme package name.
+ */
+ public final String getThemePackageName() {
+ return mPreferences.getString(PACKAGE_NAME, ELEVEN_PACKAGE);
+ }
+
+ /**
+ * Used to return a color from the theme resources.
+ *
+ * @param resourceName The name of the color to return. i.e.
+ * "action_bar_color".
+ * @return A new color from the theme resources.
+ */
+ public int getColor(final String resourceName) {
+ final int resourceId = mResources.getIdentifier(resourceName, "color", mThemePackage);
+ try {
+ return mResources.getColor(resourceId);
+ } catch (final Resources.NotFoundException e) {
+ // If the theme designer wants to allow the user to theme a
+ // particular object via the color picker, they just remove the
+ // resource item from the themeconfig.xml file.
+ }
+ return mCurrentThemeColor;
+ }
+
+ /**
+ * Used to return a drawable from the theme resources.
+ *
+ * @param resourceName The name of the drawable to return. i.e.
+ * "pager_background".
+ * @return A new color from the theme resources.
+ */
+ public Drawable getDrawable(final String resourceName) {
+ final int resourceId = mResources.getIdentifier(resourceName, "drawable", mThemePackage);
+ try {
+ return mResources.getDrawable(resourceId);
+ } catch (final Resources.NotFoundException e) {
+ //$FALL-THROUGH$
+ }
+ return null;
+ }
+
+ /**
+ * Used to tell if the action bar's backgrond color is dark or light and
+ * depending on which the proper overflow icon is set from a style.
+ *
+ * @return True if the action bar color is dark, false if light.
+ */
+ public boolean isActionBarDark() {
+ return ApolloUtils.isColorDark(getColor("action_bar"));
+ }
+
+ /**
+ * Sets the corret overflow icon in the action bar depending on whether or
+ * not the current action bar color is dark or light.
+ *
+ * @param app The {@link Activity} used to set the theme.
+ */
+ public void setOverflowStyle(final Activity app) {
+ if (isActionBarDark()) {
+ app.setTheme(R.style.Apollo_Theme_Dark);
+ } else {
+ app.setTheme(R.style.Apollo_Theme_Light);
+ }
+ }
+
+ /**
+ * This is used to set the color of a {@link MenuItem}. For instance, when
+ * the current song is a favorite, the favorite icon will use the current
+ * theme color.
+ *
+ * @param menuItem The {@link MenuItem} to set.
+ * @param resourceColorName The color theme resource key.
+ * @param resourceDrawableName The drawable theme resource key.
+ */
+ public void setMenuItemColor(final MenuItem menuItem, final String resourceColorName,
+ final String resourceDrawableName) {
+
+ final Drawable maskDrawable = getDrawable(resourceDrawableName);
+ if (!(maskDrawable instanceof BitmapDrawable)) {
+ return;
+ }
+
+ final Bitmap maskBitmap = ((BitmapDrawable)maskDrawable).getBitmap();
+ final int width = maskBitmap.getWidth();
+ final int height = maskBitmap.getHeight();
+
+ final Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(outBitmap);
+ canvas.drawBitmap(maskBitmap, 0, 0, null);
+
+ final Paint maskedPaint = new Paint();
+ maskedPaint.setColor(getColor(resourceColorName));
+ maskedPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+
+ canvas.drawRect(0, 0, width, height, maskedPaint);
+
+ final BitmapDrawable outDrawable = new BitmapDrawable(mResources, outBitmap);
+ menuItem.setIcon(outDrawable);
+ }
+
+ /**
+ * Sets the {@link MenuItem} icon for the favorites action.
+ *
+ * @param context The {@link Context} to use.
+ * @param favorite The favorites action.
+ */
+ public void setFavoriteIcon(final Menu favorite) {
+ final MenuItem favoriteAction = favorite.findItem(R.id.menu_favorite);
+ final String favoriteIconId = "ic_action_favorite";
+ if (MusicUtils.isFavorite()) {
+ setMenuItemColor(favoriteAction, "favorite_selected", favoriteIconId);
+ } else {
+ setMenuItemColor(favoriteAction, "favorite_normal", favoriteIconId);
+ }
+ }
+
+ /**
+ * Sets the {@link MenuItem} icon for the search action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "menu_search" action.
+ */
+ public void setSearchIcon(final Menu search) {
+ final MenuItem searchAction = search.findItem(R.id.menu_search);
+ final String searchIconId = "ic_action_search";
+ setMenuItemColor(searchAction, "search_action", searchIconId);
+ }
+
+ /**
+ * Sets the {@link MenuItem} icon for the shop action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "menu_shop" action.
+ */
+ public void setShopIcon(final Menu search) {
+ final MenuItem shopAction = search.findItem(R.id.menu_shop);
+ final String shopIconId = "ic_action_shop";
+ setMenuItemColor(shopAction, "shop_action", shopIconId);
+ }
+
+ /**
+ * Sets the {@link MenuItem} icon for the add to Home screen action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "add_to_homescreen" item.
+ */
+ public void setAddToHomeScreenIcon(final Menu search) {
+ final MenuItem pinnAction = search.findItem(R.id.menu_add_to_homescreen);
+ final String pinnIconId = "ic_action_pinn_to_home";
+ setMenuItemColor(pinnAction, "pinn_to_action", pinnIconId);
+ }
+
+ /**
+ * Builds a custom layout and applies it to the action bar, then themes the
+ * background, title, and subtitle.
+ *
+ * @param actionBar The {@link ActionBar} to use.
+ * @param resources The {@link ThemeUtils} used to theme the background,
+ * title, and subtitle.
+ * @param title The title for the action bar
+ * @param subtitle The subtitle for the action bar.
+ */
+ public void themeActionBar(final ActionBar actionBar, final String title) {
+ // Set the custom layout
+ actionBar.setCustomView(mActionBarLayout);
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(false);
+
+ // Theme the action bar background
+ actionBar.setBackgroundDrawable(getDrawable("action_bar"));
+
+ // Theme the title
+ setTitle(title);
+ }
+
+ /**
+ * Themes the action bar subtitle
+ *
+ * @param subtitle The subtitle to use
+ */
+ public void setTitle(final String title) {
+ if (!TextUtils.isEmpty(title)) {
+ // Get the title text view
+ final TextView actionBarTitle = (TextView)mActionBarLayout
+ .findViewById(R.id.action_bar_title);
+ // Theme the title
+ actionBarTitle.setTextColor(getColor("action_bar_title"));
+ // Set the title
+ actionBarTitle.setText(title);
+ }
+ }
+
+ /**
+ * Themes the action bar subtitle
+ *
+ * @param subtitle The subtitle to use
+ */
+ public void setSubtitle(final String subtitle) {
+ if (!TextUtils.isEmpty(subtitle)) {
+ final TextView actionBarSubtitle = (TextView)mActionBarLayout
+ .findViewById(R.id.action_bar_subtitle);
+ actionBarSubtitle.setVisibility(View.VISIBLE);
+ // Theme the subtitle
+ actionBarSubtitle.setTextColor(getColor("action_bar_subtitle"));
+ // Set the subtitle
+ actionBarSubtitle.setText(subtitle);
+ }
+ }
+
+ /**
+ * Used to search the Play Store for "Apollo Themes".
+ *
+ * @param context The {@link Context} to use.
+ */
+ public void shopFor(final Context context) {
+ final Intent shopIntent = new Intent(Intent.ACTION_VIEW);
+ shopIntent.setData(Uri.parse(String.format(SEARCH_URI, Uri.encode(sApolloSearch))));
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ context.startActivity(shopIntent);
+ }
+
+ /**
+ * Used to search the Play Store for a specific app.
+ *
+ * @param context The {@link Context} to use.
+ * @param themeName The theme name to search for.
+ */
+ public static void openAppPage(final Context context, final String themeName) {
+ final Intent shopIntent = new Intent(Intent.ACTION_VIEW);
+ shopIntent.setData(Uri.parse(APP_URI + themeName));
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ context.startActivity(shopIntent);
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/AlphaPatternDrawable.java b/src/com/cyngn/eleven/widgets/AlphaPatternDrawable.java
new file mode 100644
index 0000000..d078d26
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/AlphaPatternDrawable.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * This drawable that draws a simple white and gray chess board pattern. It's
+ * pattern you will often see as a background behind a partly transparent image
+ * in many applications.
+ *
+ * @author Daniel Nilsson
+ */
+public class AlphaPatternDrawable extends Drawable {
+
+ private final Paint mPaint = new Paint();
+
+ private final Paint mPaintWhite = new Paint();
+
+ private final Paint mPaintGray = new Paint();
+
+ private int mRectangleSize = 10;
+
+ private int numRectanglesHorizontal;
+
+ private int numRectanglesVertical;
+
+ /* Bitmap in which the pattern will be cached. */
+ private Bitmap mBitmap;
+
+ /**/
+ public AlphaPatternDrawable(final int rectangleSize) {
+ mRectangleSize = rectangleSize;
+ mPaintWhite.setColor(0xffffffff);
+ mPaintGray.setColor(0xffcbcbcb);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void draw(final Canvas canvas) {
+ canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAlpha(final int alpha) {
+ throw new UnsupportedOperationException("Alpha is not supported by this drawable.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ throw new UnsupportedOperationException("ColorFilter is not supported by this drawable.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ final int mHeight = bounds.height();
+ final int mWidth = bounds.width();
+
+ numRectanglesHorizontal = (int)Math.ceil((mWidth / mRectangleSize));
+ numRectanglesVertical = (int)Math.ceil(mHeight / mRectangleSize);
+
+ generatePatternBitmap();
+ }
+
+ /**
+ * This will generate a bitmap with the pattern as big as the rectangle we
+ * were allow to draw on. We do this to cache the bitmap so we don't need to
+ * recreate it each time draw() is called since it takes a few milliseconds.
+ */
+ private void generatePatternBitmap() {
+
+ if (getBounds().width() <= 0 || getBounds().height() <= 0) {
+ return;
+ }
+
+ mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888);
+ final Canvas mCanvas = new Canvas(mBitmap);
+
+ final Rect mRect = new Rect();
+ boolean mVerticalStartWhite = true;
+ for (int i = 0; i <= numRectanglesVertical; i++) {
+ boolean mIsWhite = mVerticalStartWhite;
+ for (int j = 0; j <= numRectanglesHorizontal; j++) {
+ mRect.top = i * mRectangleSize;
+ mRect.left = j * mRectangleSize;
+ mRect.bottom = mRect.top + mRectangleSize;
+ mRect.right = mRect.left + mRectangleSize;
+
+ mCanvas.drawRect(mRect, mIsWhite ? mPaintWhite : mPaintGray);
+
+ mIsWhite = !mIsWhite;
+ }
+ mVerticalStartWhite = !mVerticalStartWhite;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/AlphaTouchInterceptorOverlay.java b/src/com/cyngn/eleven/widgets/AlphaTouchInterceptorOverlay.java
new file mode 100644
index 0000000..b8c564f
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/AlphaTouchInterceptorOverlay.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * A View that other Views can use to create a touch-interceptor layer above
+ * their other sub-views. This layer can be enabled and disabled; when enabled,
+ * clicks are intercepted and passed to a listener. Also supports an alpha layer
+ * to dim the content underneath. By default, the alpha layer is the same View
+ * as the touch-interceptor layer. However, for some use-cases, you want a few
+ * Views to not be dimmed, but still have touches intercepted (for example,
+ * {@link CarouselTab}'s label appears above the alpha layer). In this case, you
+ * can specify the View to use as the alpha layer via setAlphaLayer(); in this
+ * case you are responsible for managing the z-order of the alpha-layer with
+ * respect to your other sub-views. Typically, you would not use this class
+ * directly, but rather use another class that uses it, for example
+ * {@link FrameLayoutWithOverlay}.
+ */
+public class AlphaTouchInterceptorOverlay extends FrameLayout {
+
+ private final View mInterceptorLayer;
+
+ private float mAlpha = 0.0f;
+
+ private View mAlphaLayer;
+
+ /**
+ * @param context The {@link Context} to use.
+ */
+ public AlphaTouchInterceptorOverlay(final Context context) {
+ super(context);
+
+ mInterceptorLayer = new View(context);
+ mInterceptorLayer.setBackgroundColor(0);
+ addView(mInterceptorLayer);
+
+ mAlphaLayer = this;
+ }
+
+ /**
+ * Set the View that the overlay will use as its alpha-layer. If none is set
+ * it will use itself. Only necessary to set this if some child views need
+ * to appear above the alpha-layer but below the touch-interceptor.
+ */
+ public void setAlphaLayer(final View alphaLayer) {
+ if (mAlphaLayer == alphaLayer) {
+ return;
+ }
+
+ /* We're no longer the alpha-layer, so make ourself invisible. */
+ if (mAlphaLayer == this) {
+ setAlphaOnViewBackground(this, 0.0f);
+ }
+
+ mAlphaLayer = alphaLayer == null ? this : alphaLayer;
+ setAlphaLayerValue(mAlpha);
+ }
+
+ /** Sets the alpha value on the alpha layer. */
+ public void setAlphaLayerValue(final float alpha) {
+ mAlpha = alpha;
+ if (mAlphaLayer != null) {
+ setAlphaOnViewBackground(mAlphaLayer, mAlpha);
+ }
+ }
+
+ /** Delegate to interceptor-layer. */
+ public void setOverlayOnClickListener(final OnClickListener listener) {
+ mInterceptorLayer.setOnClickListener(listener);
+ }
+
+ /** Delegate to interceptor-layer. */
+ public void setOverlayClickable(final boolean clickable) {
+ mInterceptorLayer.setClickable(clickable);
+ }
+
+ /**
+ * Sets an alpha value on the view.
+ */
+ public static void setAlphaOnViewBackground(final View view, final float alpha) {
+ if (view != null) {
+ view.setBackgroundColor((int)(clamp(alpha, 0.0f, 1.0f) * 255) << 24);
+ }
+ }
+
+ /**
+ * If the input value lies outside of the specified range, return the nearer
+ * bound. Otherwise, return the input value, unchanged.
+ */
+ public static float clamp(final float input, final float lowerBound, final float upperBound) {
+ if (input < lowerBound) {
+ return lowerBound;
+ } else if (input > upperBound) {
+ return upperBound;
+ }
+ return input;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/CarouselTab.java b/src/com/cyngn/eleven/widgets/CarouselTab.java
new file mode 100644
index 0000000..1b553b4
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/CarouselTab.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.BitmapUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class CarouselTab extends FrameLayoutWithOverlay {
+
+ private ImageView mPhoto;
+
+ private ImageView mAlbumArt;
+
+ private TextView mLabelView;
+
+ private View mAlphaLayer;
+
+ private View mColorstrip;
+
+ private final ImageFetcher mFetcher;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public CarouselTab(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mFetcher = ApolloUtils.getImageFetcher((Activity) context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPhoto = (ImageView)findViewById(R.id.profile_tab_photo);
+ mAlbumArt = (ImageView)findViewById(R.id.profile_tab_album_art);
+ mLabelView = (TextView)findViewById(R.id.profile_tab_label);
+ mAlphaLayer = findViewById(R.id.profile_tab_alpha_overlay);
+ mColorstrip = findViewById(R.id.profile_tab_colorstrip);
+ // Set the alpha layer
+ setAlphaLayer(mAlphaLayer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setSelected(final boolean selected) {
+ super.setSelected(selected);
+ if (selected) {
+ mColorstrip.setVisibility(View.VISIBLE);
+ } else {
+ mColorstrip.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Used to set the artist image in the artist profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The name of the artist in the profile the user is viewing.
+ */
+ public void setArtistPhoto(final Activity context, final String artist) {
+ if (!TextUtils.isEmpty(artist)) {
+ mFetcher.loadArtistImage(artist, mPhoto);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to blur the artist image in the album profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The artist nmae used to fetch the cached artist image.
+ * @param album The album name used to fetch the album art in case the
+ * artist image is missing.
+ */
+ public void blurPhoto(final Activity context, final String artist,
+ final String album) {
+ //FIXME: this should go into an AsyncTask
+
+ // First check for the artist image
+ Bitmap artistImage = mFetcher.getCachedBitmap(artist);
+ // Second check for cached artwork
+ if (artistImage == null) {
+ artistImage = mFetcher.getCachedArtwork(album, artist);
+ }
+ // If all else, use the default image
+ if (artistImage == null) {
+ artistImage = BitmapFactory.decodeResource(getResources(), R.drawable.theme_preview);
+ }
+ final Bitmap blur = BitmapUtils.createBlurredBitmap(artistImage);
+ mPhoto.setImageBitmap(blur);
+ }
+
+ /**
+ * Used to set the album art in the album profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param album The name of the album in the profile the user is viewing.
+ */
+ public void setAlbumPhoto(final Activity context, final String album, final String artist) {
+ if (!TextUtils.isEmpty(album)) {
+ mAlbumArt.setVisibility(View.VISIBLE);
+ mFetcher.loadAlbumImage(artist, album,
+ MusicUtils.getIdForAlbum(context, album, artist), mAlbumArt);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to fetch for the album art via Last.fm.
+ *
+ * @param context The {@link Context} to use.
+ * @param album The name of the album in the profile the user is viewing.
+ * @param artist The name of the album artist in the profile the user is viewing
+ */
+ public void fetchAlbumPhoto(final Activity context, final String album, final String artist) {
+ if (!TextUtils.isEmpty(album)) {
+ mFetcher.removeFromCache(ImageFetcher.generateAlbumCacheKey(album, artist));
+ mFetcher.loadAlbumImage(artist, album, -1, mAlbumArt);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to set the album art in the artist profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The name of the artist in the profile the user is viewing.
+ */
+ public void setArtistAlbumPhoto(final Activity context, final String artist) {
+ final String lastAlbum = MusicUtils.getLastAlbumForArtist(context, artist);
+ if (!TextUtils.isEmpty(lastAlbum)) {
+ // Set the last album the artist played
+ mFetcher.loadAlbumImage(artist, lastAlbum,
+ MusicUtils.getIdForAlbum(context, lastAlbum, artist), mPhoto);
+ // Play the album
+ mPhoto.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final long[] albumList = MusicUtils.getSongListForAlbum(getContext(),
+ MusicUtils.getIdForAlbum(context, lastAlbum, artist));
+ MusicUtils.playAll(getContext(), albumList, 0, false);
+ }
+ });
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to set the header image for playlists and genres.
+ *
+ * @param context The {@link Context} to use.
+ * @param profileName The key used to fetch the image.
+ */
+ public void setPlaylistOrGenrePhoto(final Activity context,
+ final String profileName) {
+ if (!TextUtils.isEmpty(profileName)) {
+ final Bitmap image = mFetcher.getCachedBitmap(profileName);
+ if (image != null) {
+ mPhoto.setImageBitmap(image);
+ } else {
+ setDefault(context);
+ }
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ */
+ public void setDefault(final Context context) {
+ mPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.header_temp));
+ }
+
+ /**
+ * @param label The string to set as the labe.
+ */
+ public void setLabel(final String label) {
+ mLabelView.setText(label);
+ }
+
+ /**
+ * Selects the label view.
+ */
+ public void showSelectedState() {
+ mLabelView.setSelected(true);
+ }
+
+ /**
+ * Deselects the label view.
+ */
+ public void showDeselectedState() {
+ mLabelView.setSelected(false);
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the header photo.
+ */
+ public ImageView getPhoto() {
+ return mPhoto;
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the album art .
+ */
+ public ImageView getAlbumArt() {
+ return mAlbumArt;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/ColorPanelView.java b/src/com/cyngn/eleven/widgets/ColorPanelView.java
new file mode 100644
index 0000000..c449e4d
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ColorPanelView.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * This class draws a panel which which will be filled with a color which can be
+ * set. It can be used to show the currently selected color which you will get
+ * from the {@link ColorPickerView}.
+ *
+ * @author Daniel Nilsson
+ */
+public class ColorPanelView extends View {
+
+ /**
+ * The width in pixels of the border surrounding the color panel.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ private static float mDensity = 1f;
+
+ private int mBorderColor = 0xff6E6E6E;
+
+ private int mColor = 0xff000000;
+
+ private Paint mBorderPaint;
+
+ private Paint mColorPaint;
+
+ private RectF mDrawingRect;
+
+ private RectF mColorRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+ public ColorPanelView(final Context context) {
+ this(context, null);
+ }
+
+ public ColorPanelView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ColorPanelView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mBorderPaint = new Paint();
+ mColorPaint = new Paint();
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ final RectF rect = mColorRect;
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect, mBorderPaint);
+ }
+
+ if (mAlphaPattern != null) {
+ mAlphaPattern.draw(canvas);
+ }
+
+ mColorPaint.setColor(mColor);
+ canvas.drawRect(rect, mColorPaint);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mDrawingRect = new RectF();
+ mDrawingRect.left = getPaddingLeft();
+ mDrawingRect.right = w - getPaddingRight();
+ mDrawingRect.top = getPaddingTop();
+ mDrawingRect.bottom = h - getPaddingBottom();
+
+ setUpColorRect();
+ }
+
+ private void setUpColorRect() {
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mColorRect = new RectF(left, top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
+
+ mAlphaPattern.setBounds(Math.round(mColorRect.left), Math.round(mColorRect.top),
+ Math.round(mColorRect.right), Math.round(mColorRect.bottom));
+ }
+
+ /**
+ * Set the color that should be shown by this view.
+ *
+ * @param color
+ */
+ public void setColor(final int color) {
+ mColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color currently show by this view.
+ *
+ * @return
+ */
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Set the color of the border surrounding the panel.
+ *
+ * @param color
+ */
+ public void setBorderColor(final int color) {
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding the panel.
+ */
+ public int getBorderColor() {
+ return mBorderColor;
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/ColorPickerView.java b/src/com/cyngn/eleven/widgets/ColorPickerView.java
new file mode 100644
index 0000000..107b94c
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ColorPickerView.java
@@ -0,0 +1,947 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposeShader;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Displays a color picker to the user and allow them to select a color. A
+ * slider for the alpha channel is also available. Enable it by setting
+ * setAlphaSliderVisible(boolean) to true.
+ *
+ * @author Daniel Nilsson
+ */
+public class ColorPickerView extends View {
+
+ public interface OnColorChangedListener {
+ public void onColorChanged(int color);
+ }
+
+ private final static int PANEL_SAT_VAL = 0;
+
+ private final static int PANEL_HUE = 1;
+
+ private final static int PANEL_ALPHA = 2;
+
+ /**
+ * The width in pixels of the border surrounding all color panels.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ /**
+ * The width in dp of the hue panel.
+ */
+ private float HUE_PANEL_WIDTH = 30f;
+
+ /**
+ * The height in dp of the alpha panel
+ */
+ private float ALPHA_PANEL_HEIGHT = 20f;
+
+ /**
+ * The distance in dp between the different color panels.
+ */
+ private float PANEL_SPACING = 10f;
+
+ /**
+ * The radius in dp of the color palette tracker circle.
+ */
+ private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f;
+
+ /**
+ * The dp which the tracker of the hue or alpha panel will extend outside of
+ * its bounds.
+ */
+ private float RECTANGLE_TRACKER_OFFSET = 2f;
+
+ private static float mDensity = 1f;
+
+ private OnColorChangedListener mListener;
+
+ private Paint mSatValPaint;
+
+ private Paint mSatValTrackerPaint;
+
+ private Paint mHuePaint;
+
+ private Paint mHueTrackerPaint;
+
+ private Paint mAlphaPaint;
+
+ private Paint mAlphaTextPaint;
+
+ private Paint mBorderPaint;
+
+ private Shader mValShader;
+
+ private Shader mSatShader;
+
+ private Shader mHueShader;
+
+ private Shader mAlphaShader;
+
+ private int mAlpha = 0xff;
+
+ private float mHue = 360f;
+
+ private float mSat = 0f;
+
+ private float mVal = 0f;
+
+ private String mAlphaSliderText = "Alpha";
+
+ private int mSliderTrackerColor = 0xff1c1c1c;
+
+ private int mBorderColor = 0xff6E6E6E;
+
+ private boolean mShowAlphaPanel = false;
+
+ /*
+ * To remember which panel that has the "focus" when processing hardware
+ * button data.
+ */
+ private int mLastTouchedPanel = PANEL_SAT_VAL;
+
+ /**
+ * Offset from the edge we must have or else the finger tracker will get
+ * clipped when it is drawn outside of the view.
+ */
+ private float mDrawingOffset;
+
+ /*
+ * Distance form the edges of the view of where we are allowed to draw.
+ */
+ private RectF mDrawingRect;
+
+ private RectF mSatValRect;
+
+ private RectF mHueRect;
+
+ private RectF mAlphaRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+ private Point mStartTouchPoint = null;
+
+ public ColorPickerView(final Context context) {
+ this(context, null);
+ }
+
+ public ColorPickerView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity;
+ RECTANGLE_TRACKER_OFFSET *= mDensity;
+ HUE_PANEL_WIDTH *= mDensity;
+ ALPHA_PANEL_HEIGHT *= mDensity;
+ PANEL_SPACING = PANEL_SPACING * mDensity;
+
+ mDrawingOffset = calculateRequiredOffset();
+
+ initPaintTools();
+
+ // Needed for receiving track ball motion events.
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ }
+
+ private void initPaintTools() {
+
+ mSatValPaint = new Paint();
+ mSatValTrackerPaint = new Paint();
+ mHuePaint = new Paint();
+ mHueTrackerPaint = new Paint();
+ mAlphaPaint = new Paint();
+ mAlphaTextPaint = new Paint();
+ mBorderPaint = new Paint();
+
+ mSatValTrackerPaint.setStyle(Style.STROKE);
+ mSatValTrackerPaint.setStrokeWidth(2f * mDensity);
+ mSatValTrackerPaint.setAntiAlias(true);
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+ mHueTrackerPaint.setStyle(Style.STROKE);
+ mHueTrackerPaint.setStrokeWidth(2f * mDensity);
+ mHueTrackerPaint.setAntiAlias(true);
+
+ mAlphaTextPaint.setColor(0xff1c1c1c);
+ mAlphaTextPaint.setTextSize(14f * mDensity);
+ mAlphaTextPaint.setAntiAlias(true);
+ mAlphaTextPaint.setTextAlign(Align.CENTER);
+ mAlphaTextPaint.setFakeBoldText(true);
+
+ }
+
+ private float calculateRequiredOffset() {
+ float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET);
+ offset = Math.max(offset, BORDER_WIDTH_PX * mDensity);
+
+ return offset * 1.5f;
+ }
+
+ private int[] buildHueColorArray() {
+
+ final int[] hue = new int[361];
+
+ int count = 0;
+ for (int i = hue.length - 1; i >= 0; i--, count++) {
+ hue[count] = Color.HSVToColor(new float[] {
+ i, 1f, 1f
+ });
+ }
+
+ return hue;
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+
+ if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) {
+ return;
+ }
+
+ drawSatValPanel(canvas);
+ drawHuePanel(canvas);
+ drawAlphaPanel(canvas);
+
+ }
+
+ private void drawSatValPanel(final Canvas canvas) {
+
+ final RectF rect = mSatValRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX,
+ rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ if (mValShader == null) {
+ mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
+ 0xffffffff, 0xff000000, TileMode.CLAMP);
+ }
+
+ final int rgb = Color.HSVToColor(new float[] {
+ mHue, 1f, 1f
+ });
+
+ mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb,
+ TileMode.CLAMP);
+ final ComposeShader mShader = new ComposeShader(mValShader, mSatShader,
+ PorterDuff.Mode.MULTIPLY);
+ mSatValPaint.setShader(mShader);
+
+ canvas.drawRect(rect, mSatValPaint);
+
+ final Point p = satValToPoint(mSat, mVal);
+
+ mSatValTrackerPaint.setColor(0xff000000);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity,
+ mSatValTrackerPaint);
+
+ mSatValTrackerPaint.setColor(0xffdddddd);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint);
+
+ }
+
+ private void drawHuePanel(final Canvas canvas) {
+
+ final RectF rect = mHueRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
+ + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ if (mHueShader == null) {
+ mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
+ buildHueColorArray(), null, TileMode.CLAMP);
+ mHuePaint.setShader(mHueShader);
+ }
+
+ canvas.drawRect(rect, mHuePaint);
+
+ final float rectHeight = 4 * mDensity / 2;
+
+ final Point p = hueToPoint(mHue);
+
+ final RectF r = new RectF();
+ r.left = rect.left - RECTANGLE_TRACKER_OFFSET;
+ r.right = rect.right + RECTANGLE_TRACKER_OFFSET;
+ r.top = p.y - rectHeight;
+ r.bottom = p.y + rectHeight;
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+ private void drawAlphaPanel(final Canvas canvas) {
+
+ if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) {
+ return;
+ }
+
+ final RectF rect = mAlphaRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
+ + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ mAlphaPattern.draw(canvas);
+
+ final float[] hsv = new float[] {
+ mHue, mSat, mVal
+ };
+ final int color = Color.HSVToColor(hsv);
+ final int acolor = Color.HSVToColor(0, hsv);
+
+ mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor,
+ TileMode.CLAMP);
+
+ mAlphaPaint.setShader(mAlphaShader);
+
+ canvas.drawRect(rect, mAlphaPaint);
+
+ if (mAlphaSliderText != null && mAlphaSliderText != "") {
+ canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity,
+ mAlphaTextPaint);
+ }
+
+ final float rectWidth = 4 * mDensity / 2;
+
+ final Point p = alphaToPoint(mAlpha);
+
+ final RectF r = new RectF();
+ r.left = p.x - rectWidth;
+ r.right = p.x + rectWidth;
+ r.top = rect.top - RECTANGLE_TRACKER_OFFSET;
+ r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET;
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+ private Point hueToPoint(final float hue) {
+
+ final RectF rect = mHueRect;
+ final float height = rect.height();
+
+ final Point p = new Point();
+
+ p.y = (int)(height - hue * height / 360f + rect.top);
+ p.x = (int)rect.left;
+
+ return p;
+ }
+
+ private Point satValToPoint(final float sat, final float val) {
+
+ final RectF rect = mSatValRect;
+ final float height = rect.height();
+ final float width = rect.width();
+
+ final Point p = new Point();
+
+ p.x = (int)(sat * width + rect.left);
+ p.y = (int)((1f - val) * height + rect.top);
+
+ return p;
+ }
+
+ private Point alphaToPoint(final int alpha) {
+
+ final RectF rect = mAlphaRect;
+ final float width = rect.width();
+
+ final Point p = new Point();
+
+ p.x = (int)(width - alpha * width / 0xff + rect.left);
+ p.y = (int)rect.top;
+
+ return p;
+
+ }
+
+ private float[] pointToSatVal(float x, float y) {
+
+ final RectF rect = mSatValRect;
+ final float[] result = new float[2];
+
+ final float width = rect.width();
+ final float height = rect.height();
+
+ if (x < rect.left) {
+ x = 0f;
+ } else if (x > rect.right) {
+ x = width;
+ } else {
+ x = x - rect.left;
+ }
+
+ if (y < rect.top) {
+ y = 0f;
+ } else if (y > rect.bottom) {
+ y = height;
+ } else {
+ y = y - rect.top;
+ }
+
+ result[0] = 1.f / width * x;
+ result[1] = 1.f - 1.f / height * y;
+
+ return result;
+ }
+
+ private float pointToHue(float y) {
+
+ final RectF rect = mHueRect;
+
+ final float height = rect.height();
+
+ if (y < rect.top) {
+ y = 0f;
+ } else if (y > rect.bottom) {
+ y = height;
+ } else {
+ y = y - rect.top;
+ }
+
+ return 360f - y * 360f / height;
+ }
+
+ private int pointToAlpha(int x) {
+
+ final RectF rect = mAlphaRect;
+ final int width = (int)rect.width();
+
+ if (x < rect.left) {
+ x = 0;
+ } else if (x > rect.right) {
+ x = width;
+ } else {
+ x = x - (int)rect.left;
+ }
+
+ return 0xff - x * 0xff / width;
+
+ }
+
+ @Override
+ public boolean onTrackballEvent(final MotionEvent event) {
+
+ final float x = event.getX();
+ final float y = event.getY();
+
+ boolean update = false;
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+
+ switch (mLastTouchedPanel) {
+
+ case PANEL_SAT_VAL:
+
+ float sat,
+ val;
+
+ sat = mSat + x / 50f;
+ val = mVal - y / 50f;
+
+ if (sat < 0f) {
+ sat = 0f;
+ } else if (sat > 1f) {
+ sat = 1f;
+ }
+
+ if (val < 0f) {
+ val = 0f;
+ } else if (val > 1f) {
+ val = 1f;
+ }
+
+ mSat = sat;
+ mVal = val;
+
+ update = true;
+
+ break;
+
+ case PANEL_HUE:
+
+ float hue = mHue - y * 10f;
+
+ if (hue < 0f) {
+ hue = 0f;
+ } else if (hue > 360f) {
+ hue = 360f;
+ }
+
+ mHue = hue;
+
+ update = true;
+
+ break;
+
+ case PANEL_ALPHA:
+
+ if (!mShowAlphaPanel || mAlphaRect == null) {
+ update = false;
+ } else {
+
+ int alpha = (int)(mAlpha - x * 10);
+
+ if (alpha < 0) {
+ alpha = 0;
+ } else if (alpha > 0xff) {
+ alpha = 0xff;
+ }
+
+ mAlpha = alpha;
+
+ update = true;
+ }
+
+ break;
+ }
+
+ }
+
+ if (update) {
+
+ if (mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ return true;
+ }
+
+ return super.onTrackballEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+
+ boolean update = false;
+
+ switch (event.getAction()) {
+
+ case MotionEvent.ACTION_DOWN:
+
+ mStartTouchPoint = new Point((int)event.getX(), (int)event.getY());
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_UP:
+
+ mStartTouchPoint = null;
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ }
+
+ if (update) {
+
+ if (mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ return true;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ private boolean moveTrackersIfNeeded(final MotionEvent event) {
+
+ if (mStartTouchPoint == null) {
+ return false;
+ }
+
+ boolean update = false;
+
+ final int startX = mStartTouchPoint.x;
+ final int startY = mStartTouchPoint.y;
+
+ if (mHueRect.contains(startX, startY)) {
+ mLastTouchedPanel = PANEL_HUE;
+
+ mHue = pointToHue(event.getY());
+
+ update = true;
+ } else if (mSatValRect.contains(startX, startY)) {
+
+ mLastTouchedPanel = PANEL_SAT_VAL;
+
+ final float[] result = pointToSatVal(event.getX(), event.getY());
+
+ mSat = result[0];
+ mVal = result[1];
+
+ update = true;
+ } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) {
+
+ mLastTouchedPanel = PANEL_ALPHA;
+
+ mAlpha = pointToAlpha((int)event.getX());
+
+ update = true;
+ }
+
+ return update;
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+
+ int width = 0;
+ int height = 0;
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ int widthAllowed = MeasureSpec.getSize(widthMeasureSpec);
+ int heightAllowed = MeasureSpec.getSize(heightMeasureSpec);
+
+ widthAllowed = chooseWidth(widthMode, widthAllowed);
+ heightAllowed = chooseHeight(heightMode, heightAllowed);
+
+ if (!mShowAlphaPanel) {
+ height = (int)(widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH);
+
+ // If calculated height (based on the width) is more than the
+ // allowed height.
+ if (height > heightAllowed) {
+ height = heightAllowed;
+ width = (int)(height + PANEL_SPACING + HUE_PANEL_WIDTH);
+ } else {
+ width = widthAllowed;
+ }
+ } else {
+
+ width = (int)(heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH);
+
+ if (width > widthAllowed) {
+ width = widthAllowed;
+ height = (int)(widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT);
+ } else {
+ height = heightAllowed;
+ }
+
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int chooseWidth(final int mode, final int size) {
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedWidth();
+ }
+ }
+
+ private int chooseHeight(final int mode, final int size) {
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedHeight();
+ }
+ }
+
+ private int getPrefferedWidth() {
+
+ int width = getPrefferedHeight();
+
+ if (mShowAlphaPanel) {
+ width -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ return (int)(width + HUE_PANEL_WIDTH + PANEL_SPACING);
+
+ }
+
+ private int getPrefferedHeight() {
+
+ int height = (int)(200 * mDensity);
+
+ if (mShowAlphaPanel) {
+ height += PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ return height;
+ }
+
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mDrawingRect = new RectF();
+ mDrawingRect.left = mDrawingOffset + getPaddingLeft();
+ mDrawingRect.right = w - mDrawingOffset - getPaddingRight();
+ mDrawingRect.top = mDrawingOffset + getPaddingTop();
+ mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom();
+
+ setUpSatValRect();
+ setUpHueRect();
+ setUpAlphaRect();
+ }
+
+ private void setUpSatValRect() {
+
+ final RectF dRect = mDrawingRect;
+ float panelSide = dRect.height() - BORDER_WIDTH_PX * 2;
+
+ if (mShowAlphaPanel) {
+ panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = top + panelSide;
+ final float right = left + panelSide;
+
+ mSatValRect = new RectF(left, top, right, bottom);
+ }
+
+ private void setUpHueRect() {
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX
+ - (mShowAlphaPanel ? PANEL_SPACING + ALPHA_PANEL_HEIGHT : 0);
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mHueRect = new RectF(left, top, right, bottom);
+ }
+
+ private void setUpAlphaRect() {
+
+ if (!mShowAlphaPanel) {
+ return;
+ }
+
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mAlphaRect = new RectF(left, top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
+ mAlphaPattern.setBounds(Math.round(mAlphaRect.left), Math.round(mAlphaRect.top),
+ Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom));
+
+ }
+
+ /**
+ * Set a OnColorChangedListener to get notified when the color selected by
+ * the user has changed.
+ *
+ * @param listener
+ */
+ public void setOnColorChangedListener(final OnColorChangedListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Set the color of the border surrounding all panels.
+ *
+ * @param color
+ */
+ public void setBorderColor(final int color) {
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding all panels.
+ */
+ public int getBorderColor() {
+ return mBorderColor;
+ }
+
+ /**
+ * Get the current color this view is showing.
+ *
+ * @return the current color.
+ */
+ public int getColor() {
+ return Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ });
+ }
+
+ /**
+ * Set the color the view should show.
+ *
+ * @param color The color that should be selected.
+ */
+ public void setColor(final int color) {
+ setColor(color, false);
+ }
+
+ /**
+ * Set the color this view should show.
+ *
+ * @param color The color that should be selected.
+ * @param callback If you want to get a callback to your
+ * OnColorChangedListener.
+ */
+ public void setColor(final int color, final boolean callback) {
+
+ final int alpha = Color.alpha(color);
+ final int red = Color.red(color);
+ final int blue = Color.blue(color);
+ final int green = Color.green(color);
+
+ final float[] hsv = new float[3];
+
+ Color.RGBToHSV(red, green, blue, hsv);
+
+ mAlpha = alpha;
+ mHue = hsv[0];
+ mSat = hsv[1];
+ mVal = hsv[2];
+
+ if (callback && mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Get the drawing offset of the color picker view. The drawing offset is
+ * the distance from the side of a panel to the side of the view minus the
+ * padding. Useful if you want to have your own panel below showing the
+ * currently selected color and want to align it perfectly.
+ *
+ * @return The offset in pixels.
+ */
+ public float getDrawingOffset() {
+ return mDrawingOffset;
+ }
+
+ /**
+ * Set if the user is allowed to adjust the alpha panel. Default is false.
+ * If it is set to false no alpha will be set.
+ *
+ * @param visible
+ */
+ public void setAlphaSliderVisible(final boolean visible) {
+
+ if (mShowAlphaPanel != visible) {
+ mShowAlphaPanel = visible;
+
+ /*
+ * Reset all shader to force a recreation. Otherwise they will not
+ * look right after the size of the view has changed.
+ */
+ mValShader = null;
+ mSatShader = null;
+ mHueShader = null;
+ mAlphaShader = null;
+
+ requestLayout();
+ }
+
+ }
+
+ public void setSliderTrackerColor(final int color) {
+ mSliderTrackerColor = color;
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+
+ invalidate();
+ }
+
+ public int getSliderTrackerColor() {
+ return mSliderTrackerColor;
+ }
+
+ /**
+ * Set the text that should be shown in the alpha slider. Set to null to
+ * disable text.
+ *
+ * @param res string resource id.
+ */
+ public void setAlphaSliderText(final int res) {
+ final String text = getContext().getString(res);
+ setAlphaSliderText(text);
+ }
+
+ /**
+ * Set the text that should be shown in the alpha slider. Set to null to
+ * disable text.
+ *
+ * @param text Text that should be shown.
+ */
+ public void setAlphaSliderText(final String text) {
+ mAlphaSliderText = text;
+ invalidate();
+ }
+
+ /**
+ * Get the current value of the text that will be shown in the alpha slider.
+ *
+ * @return
+ */
+ public String getAlphaSliderText() {
+ return mAlphaSliderText;
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/ColorSchemeDialog.java b/src/com/cyngn/eleven/widgets/ColorSchemeDialog.java
new file mode 100644
index 0000000..fa13fc6
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ColorSchemeDialog.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.widgets.ColorPickerView.OnColorChangedListener;
+
+import java.util.Locale;
+
+/**
+ * Shows the {@link ColorPanelView} in a new {@link AlertDialog}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ColorSchemeDialog extends AlertDialog implements
+ ColorPickerView.OnColorChangedListener {
+
+ private final int mCurrentColor;
+
+ private final OnColorChangedListener mListener = this;;
+
+ private LayoutInflater mInflater;
+
+ private ColorPickerView mColorPicker;
+
+ private Button mOldColor;
+
+ private Button mNewColor;
+
+ private View mRootView;
+
+ private EditText mHexValue;
+
+ /**
+ * Constructor of <code>ColorSchemeDialog</code>
+ *
+ * @param context The {@link Contxt} to use.
+ */
+ public ColorSchemeDialog(final Context context) {
+ super(context);
+ getWindow().setFormat(PixelFormat.RGBA_8888);
+ mCurrentColor = PreferenceUtils.getInstance(context).getDefaultThemeColor(context);
+ setUp(mCurrentColor);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ApolloUtils.removeHardwareAccelerationSupport(mColorPicker);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.cyngn.eleven.widgets.ColorPickerView.OnColorChangedListener#
+ * onColorChanged(int)
+ */
+ @Override
+ public void onColorChanged(final int color) {
+ if (mHexValue != null) {
+ mHexValue.setText(padLeft(Integer.toHexString(color).toUpperCase(Locale.getDefault()),
+ '0', 8));
+ }
+ mNewColor.setBackgroundColor(color);
+ }
+
+ private String padLeft(final String string, final char padChar, final int size) {
+ if (string.length() >= size) {
+ return string;
+ }
+ final StringBuilder result = new StringBuilder();
+ for (int i = string.length(); i < size; i++) {
+ result.append(padChar);
+ }
+ result.append(string);
+ return result.toString();
+ }
+
+ /**
+ * Initialzes the presets and color picker
+ *
+ * @param color The color to use.
+ */
+ private void setUp(final int color) {
+ mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mRootView = mInflater.inflate(R.layout.color_scheme_dialog, null);
+
+ mColorPicker = (ColorPickerView)mRootView.findViewById(R.id.color_picker_view);
+ mOldColor = (Button)mRootView.findViewById(R.id.color_scheme_dialog_old_color);
+ mOldColor.setOnClickListener(mPresetListener);
+ mNewColor = (Button)mRootView.findViewById(R.id.color_scheme_dialog_new_color);
+ setUpPresets(R.id.color_scheme_dialog_preset_one);
+ setUpPresets(R.id.color_scheme_dialog_preset_two);
+ setUpPresets(R.id.color_scheme_dialog_preset_three);
+ setUpPresets(R.id.color_scheme_dialog_preset_four);
+ setUpPresets(R.id.color_scheme_dialog_preset_five);
+ setUpPresets(R.id.color_scheme_dialog_preset_six);
+ setUpPresets(R.id.color_scheme_dialog_preset_seven);
+ setUpPresets(R.id.color_scheme_dialog_preset_eight);
+ mHexValue = (EditText)mRootView.findViewById(R.id.color_scheme_dialog_hex_value);
+ mHexValue.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ try {
+ mColorPicker.setColor(Color.parseColor("#"
+ + mHexValue.getText().toString().toUpperCase(Locale.getDefault())));
+ mNewColor.setBackgroundColor(Color.parseColor("#"
+ + mHexValue.getText().toString().toUpperCase(Locale.getDefault())));
+ } catch (final Exception ignored) {
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ /* Nothing to do */
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ /* Nothing to do */
+ }
+ });
+
+ mColorPicker.setOnColorChangedListener(this);
+ mOldColor.setBackgroundColor(color);
+ mColorPicker.setColor(color, true);
+
+ setTitle(R.string.color_picker_title);
+ setView(mRootView);
+ }
+
+ /**
+ * @param color The color resource.
+ * @return A new color from Apollo's resources.
+ */
+ private int getColor(final int color) {
+ return getContext().getResources().getColor(color);
+ }
+
+ /**
+ * @return {@link ColorPickerView}'s current color
+ */
+ public int getColor() {
+ return mColorPicker.getColor();
+ }
+
+ /**
+ * @param which The Id of the preset color
+ */
+ private void setUpPresets(final int which) {
+ final Button preset = (Button)mRootView.findViewById(which);
+ if (preset != null) {
+ preset.setOnClickListener(mPresetListener);
+ }
+ }
+
+ /**
+ * Sets up the preset buttons
+ */
+ private final View.OnClickListener mPresetListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ switch (v.getId()) {
+ case R.id.color_scheme_dialog_preset_one:
+ mColorPicker.setColor(getColor(R.color.holo_blue_light));
+ break;
+ case R.id.color_scheme_dialog_preset_two:
+ mColorPicker.setColor(getColor(R.color.holo_green_light));
+ break;
+ case R.id.color_scheme_dialog_preset_three:
+ mColorPicker.setColor(getColor(R.color.holo_orange_dark));
+ break;
+ case R.id.color_scheme_dialog_preset_four:
+ mColorPicker.setColor(getColor(R.color.holo_orange_light));
+ break;
+ case R.id.color_scheme_dialog_preset_five:
+ mColorPicker.setColor(getColor(R.color.holo_purple));
+ break;
+ case R.id.color_scheme_dialog_preset_six:
+ mColorPicker.setColor(getColor(R.color.holo_red_light));
+ break;
+ case R.id.color_scheme_dialog_preset_seven:
+ mColorPicker.setColor(getColor(R.color.white));
+ break;
+ case R.id.color_scheme_dialog_preset_eight:
+ mColorPicker.setColor(getColor(R.color.black));
+ break;
+ case R.id.color_scheme_dialog_old_color:
+ mColorPicker.setColor(mCurrentColor);
+ break;
+ default:
+ break;
+ }
+ if (mListener != null) {
+ mListener.onColorChanged(getColor());
+ }
+ }
+ };
+
+}
diff --git a/src/com/cyngn/eleven/widgets/FrameLayoutWithOverlay.java b/src/com/cyngn/eleven/widgets/FrameLayoutWithOverlay.java
new file mode 100644
index 0000000..06c3eaf
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/FrameLayoutWithOverlay.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * A FrameLayout whose contents are kept beneath an
+ * {@link AlphaTouchInterceptorOverlay}. If necessary, you can specify your own
+ * alpha-layer and manually manage its z-order.
+ */
+public class FrameLayoutWithOverlay extends FrameLayout {
+
+ private final AlphaTouchInterceptorOverlay mOverlay;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public FrameLayoutWithOverlay(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ /* Programmatically create touch-interceptor View. */
+ mOverlay = new AlphaTouchInterceptorOverlay(context);
+
+ addView(mOverlay);
+ }
+
+ /**
+ * After adding the View, bring the overlay to the front to ensure it's
+ * always on top.
+ */
+ @Override
+ public void addView(final View child, final int index, final ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ mOverlay.bringToFront();
+ }
+
+ /**
+ * Delegate to overlay: set the View that it will use as its alpha-layer. If
+ * none is set, the overlay will use its own alpha layer. Only necessary to
+ * set this if some child views need to appear above the alpha-layer.
+ */
+ protected void setAlphaLayer(final View layer) {
+ mOverlay.setAlphaLayer(layer);
+ }
+
+ /** Delegate to overlay: set the alpha value on the alpha layer. */
+ public void setAlphaLayerValue(final float alpha) {
+ mOverlay.setAlphaLayerValue(alpha);
+ }
+
+ /** Delegate to overlay. */
+ public void setOverlayOnClickListener(final OnClickListener listener) {
+ mOverlay.setOverlayOnClickListener(listener);
+ }
+
+ /** Delegate to overlay. */
+ public void setOverlayClickable(final boolean clickable) {
+ mOverlay.setOverlayClickable(clickable);
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/LayoutSuppressingImageView.java b/src/com/cyngn/eleven/widgets/LayoutSuppressingImageView.java
new file mode 100644
index 0000000..aad2b9d
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/LayoutSuppressingImageView.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A custom {@link ImageView} that improves the performance by not passing
+ * requestLayout() to its parent, taking advantage of knowing that image size
+ * won't change once set.
+ */
+public class LayoutSuppressingImageView extends ImageView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view
+ */
+ public LayoutSuppressingImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/PlayPauseButton.java b/src/com/cyngn/eleven/widgets/PlayPauseButton.java
new file mode 100644
index 0000000..d1f77fa
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/PlayPauseButton.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.theme.HoloSelector;
+
+/**
+ * A custom {@link ImageButton} that represents the "play and pause" button.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlayPauseButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Play button theme resource
+ */
+ private static final String PLAY = "btn_playback_play";
+
+ /**
+ * Pause button theme resource
+ */
+ private static final String PAUSE = "btn_playback_pause";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public PlayPauseButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (play/pause)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.playOrPause();
+ updateState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for playback.
+ */
+ public void updateState() {
+ if (MusicUtils.isPlaying()) {
+ setContentDescription(getResources().getString(R.string.accessibility_pause));
+ setImageDrawable(mResources.getDrawable(PAUSE));
+ } else {
+ setContentDescription(getResources().getString(R.string.accessibility_play));
+ setImageDrawable(mResources.getDrawable(PLAY));
+ }
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/ProfileTabCarousel.java b/src/com/cyngn/eleven/widgets/ProfileTabCarousel.java
new file mode 100644
index 0000000..825802a
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ProfileTabCarousel.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.animation.AnimationUtils;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.ProfileActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+
+/**
+ * A custom {@link HorizontalScrollView} that displays up to two "tabs" in the
+ * {@link ProfileActivity}. If the second tab is visible, a fraction of it will
+ * overflow slightly onto the screen.
+ */
+@SuppressLint("NewApi")
+public class ProfileTabCarousel extends HorizontalScrollView implements OnTouchListener {
+
+ /**
+ * Number of tabs
+ */
+ private static final int TAB_COUNT = 2;
+
+ /**
+ * First tab index
+ */
+ private static final int TAB_INDEX_FIRST = 0;
+
+ /**
+ * Second tab index
+ */
+ private static final int TAB_INDEX_SECOND = 1;
+
+ /**
+ * Alpha layer to be set on the lable view
+ */
+ private static final float MAX_ALPHA = 0.6f;
+
+ /**
+ * Y coordinate of the tab at the given index was selected
+ */
+ private static final float[] mYCoordinateArray = new float[TAB_COUNT];
+
+ /**
+ * Tab width as defined as a fraction of the screen width
+ */
+ private final float tabWidthScreenWidthFraction;
+
+ /**
+ * Tab height as defined as a fraction of the screen width
+ */
+ private final float tabHeightScreenWidthFraction;
+
+ /**
+ * Height of the tab label
+ */
+ private final int mTabDisplayLabelHeight;
+
+ /**
+ * Height in pixels of the shadow under the tab carousel
+ */
+ private final int mTabShadowHeight;
+
+ /**
+ * First tab click listener
+ */
+ private final TabClickListener mTabOneTouchInterceptListener = new TabClickListener(
+ TAB_INDEX_FIRST);
+
+ /**
+ * Second tab click listener
+ */
+ private final TabClickListener mTabTwoTouchInterceptListener = new TabClickListener(
+ TAB_INDEX_SECOND);
+
+ /**
+ * The last scrolled position
+ */
+ private int mLastScrollPosition = Integer.MIN_VALUE;
+
+ /**
+ * Allowed horizontal scroll length
+ */
+ private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
+
+ /**
+ * Allowed vertical scroll length
+ */
+ private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
+
+ /**
+ * Current tab index
+ */
+ private int mCurrentTab = TAB_INDEX_FIRST;
+
+ /**
+ * Factor to scale scroll-amount sent to listeners
+ */
+ private float mScrollScaleFactor = 1.0f;
+
+ private boolean mScrollToCurrentTab = false;
+
+ private boolean mTabCarouselIsAnimating;
+
+ private boolean mEnableSwipe;
+
+ private CarouselTab mFirstTab;
+
+ private CarouselTab mSecondTab;
+
+ private Listener mListener;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ProfileTabCarousel(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setOnTouchListener(this);
+ final Resources mResources = context.getResources();
+ mTabDisplayLabelHeight = mResources
+ .getDimensionPixelSize(R.dimen.profile_photo_shadow_height);
+ mTabShadowHeight = mResources.getDimensionPixelSize(R.dimen.profile_carousel_label_height);
+ tabWidthScreenWidthFraction = mResources.getFraction(
+ R.fraction.tab_width_screen_percentage, 1, 1);
+ tabHeightScreenWidthFraction = mResources.getFraction(
+ R.fraction.tab_height_screen_percentage, 1, 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mFirstTab = (CarouselTab)findViewById(R.id.profile_tab_carousel_tab_one);
+ mFirstTab.setOverlayOnClickListener(mTabOneTouchInterceptListener);
+ mSecondTab = (CarouselTab)findViewById(R.id.profile_tab_carousel_tab_two);
+ mSecondTab.setOverlayOnClickListener(mTabTwoTouchInterceptListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int tabWidth = Math.round(tabWidthScreenWidthFraction * screenWidth);
+
+ mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
+ if (mAllowedHorizontalScrollLength == 0) {
+ mScrollScaleFactor = 1.0f;
+ } else {
+ mScrollScaleFactor = screenWidth / mAllowedHorizontalScrollLength;
+ }
+
+ final int tabHeight = Math.round(screenWidth * tabHeightScreenWidthFraction)
+ + mTabShadowHeight;
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+
+ // Add 1 dip of separation between the tabs
+ final int seperatorPixels = (int)(TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()) + 0.5f);
+
+ if (mEnableSwipe) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(TAB_COUNT * tabWidth + (TAB_COUNT - 1)
+ * seperatorPixels, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
+ } else {
+ child.measure(MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
+ }
+ }
+ mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight - mTabShadowHeight;
+ setMeasuredDimension(resolveSize(screenWidth, widthMeasureSpec),
+ resolveSize(tabHeight, heightMeasureSpec));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressLint("DrawAllocation")
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r,
+ final int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (!mScrollToCurrentTab) {
+ return;
+ }
+ mScrollToCurrentTab = false;
+ ApolloUtils.doAfterLayout(this, new Runnable() {
+ @Override
+ public void run() {
+ scrollTo(mCurrentTab == TAB_INDEX_FIRST ? 0 : mAllowedHorizontalScrollLength, 0);
+ updateAlphaLayers();
+ }
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onScrollChanged(final int x, final int y, final int oldX, final int oldY) {
+ super.onScrollChanged(x, y, oldX, oldY);
+ if (mLastScrollPosition == x) {
+ return;
+ }
+ final int scaledL = (int)(x * mScrollScaleFactor);
+ final int oldScaledL = (int)(oldX * mScrollScaleFactor);
+ mListener.onScrollChanged(scaledL, y, oldScaledL, oldY);
+ mLastScrollPosition = x;
+ updateAlphaLayers();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouch(final View v, final MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mListener.onTouchDown();
+ return true;
+ case MotionEvent.ACTION_UP:
+ mListener.onTouchUp();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent ev) {
+ final boolean mInterceptTouch = super.onInterceptTouchEvent(ev);
+ if (mInterceptTouch) {
+ mListener.onTouchDown();
+ }
+ return mInterceptTouch;
+ }
+
+ /**
+ * Reset the carousel to the start position (i.e. because new data will be
+ * loaded in for a different contact).
+ */
+ public void reset() {
+ scrollTo(0, 0);
+ setCurrentTab(TAB_INDEX_FIRST);
+ moveToYCoordinate(TAB_INDEX_FIRST, 0);
+ }
+
+ /**
+ * Set the current tab that should be restored when the view is first laid
+ * out.
+ */
+ public void restoreCurrentTab(final int position) {
+ setCurrentTab(position);
+ mScrollToCurrentTab = true;
+ }
+
+ /**
+ * Restore the Y position of this view to the last manually requested value.
+ * This can be done after the parent has been re-laid out again, where this
+ * view's position could have been lost if the view laid outside its
+ * parent's bounds.
+ */
+ public void restoreYCoordinate(final int duration, final int tabIndex) {
+ final float storedYCoordinate = getStoredYCoordinateForTab(tabIndex);
+
+ final ObjectAnimator tabCarouselAnimator = ObjectAnimator.ofFloat(this, "y",
+ storedYCoordinate);
+ tabCarouselAnimator.addListener(mTabCarouselAnimatorListener);
+ tabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator(getContext(),
+ android.R.anim.accelerate_decelerate_interpolator));
+ tabCarouselAnimator.setDuration(duration);
+ tabCarouselAnimator.start();
+ }
+
+ /**
+ * Request that the view move to the given Y coordinate. Also store the Y
+ * coordinate as the last requested Y coordinate for the given tabIndex.
+ */
+ public void moveToYCoordinate(final int tabIndex, final float y) {
+ storeYCoordinate(tabIndex, y);
+ restoreYCoordinate(0, tabIndex);
+ }
+
+ /**
+ * Store this information as the last requested Y coordinate for the given
+ * tabIndex.
+ */
+ public void storeYCoordinate(final int tabIndex, final float y) {
+ mYCoordinateArray[tabIndex] = y;
+ }
+
+ /**
+ * Returns the stored Y coordinate of this view the last time the user was
+ * on the selected tab given by tabIndex.
+ */
+ public float getStoredYCoordinateForTab(final int tabIndex) {
+ return mYCoordinateArray[tabIndex];
+ }
+
+ /**
+ * Returns the number of pixels that this view can be scrolled horizontally.
+ */
+ public int getAllowedHorizontalScrollLength() {
+ return mAllowedHorizontalScrollLength;
+ }
+
+ /**
+ * Returns the number of pixels that this view can be scrolled vertically
+ * while still allowing the tab labels to still show.
+ */
+ public int getAllowedVerticalScrollLength() {
+ return mAllowedVerticalScrollLength;
+ }
+
+ /**
+ * Sets the correct alpha layers over the tabs.
+ */
+ private void updateAlphaLayers() {
+ float alpha = mLastScrollPosition * MAX_ALPHA / mAllowedHorizontalScrollLength;
+ alpha = AlphaTouchInterceptorOverlay.clamp(alpha, 0.0f, 1.0f);
+ mFirstTab.setAlphaLayerValue(alpha);
+ mSecondTab.setAlphaLayerValue(MAX_ALPHA - alpha);
+ }
+
+ /**
+ * @return The {@link ImageView} in the first index.
+ */
+ public ImageView getPhoto() {
+ return mFirstTab.getPhoto();
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the album art.
+ */
+ public ImageView getAlbumArt() {
+ return mFirstTab.getAlbumArt();
+ }
+
+ /**
+ * This listener keeps track of whether the tab carousel animation is
+ * currently going on or not, in order to prevent other simultaneous changes
+ * to the Y position of the tab carousel which can cause flicker.
+ */
+ private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() {
+
+ @Override
+ public void onAnimationCancel(final Animator animation) {
+ mTabCarouselIsAnimating = false;
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ mTabCarouselIsAnimating = false;
+ }
+
+ @Override
+ public void onAnimationRepeat(final Animator animation) {
+ mTabCarouselIsAnimating = true;
+ }
+
+ @Override
+ public void onAnimationStart(final Animator animation) {
+ mTabCarouselIsAnimating = true;
+ }
+ };
+
+ /**
+ * @return True if the carousel is currently animating, false otherwise
+ */
+ public boolean isTabCarouselIsAnimating() {
+ return mTabCarouselIsAnimating;
+ }
+
+ /**
+ * Updates the tab selection.
+ */
+ public void setCurrentTab(final int position) {
+ final CarouselTab selected, deselected;
+
+ switch (position) {
+ case TAB_INDEX_FIRST:
+ selected = mFirstTab;
+ deselected = mSecondTab;
+ break;
+ case TAB_INDEX_SECOND:
+ selected = mSecondTab;
+ deselected = mFirstTab;
+ break;
+ default:
+ throw new IllegalStateException("Invalid tab position " + position);
+ }
+ selected.setSelected(true);
+ selected.showSelectedState();
+ selected.setOverlayClickable(false);
+ deselected.setSelected(false);
+ deselected.showDeselectedState();
+ deselected.setOverlayClickable(true);
+ mCurrentTab = position;
+ }
+
+ /**
+ * Set the given {@link Listener} to handle carousel events.
+ */
+ public void setListener(final Listener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the artist image header
+ *
+ * @param context The {@link Activity} to use
+ * @param artistName The artist name used to find the cached artist image
+ * and used to find the last album played by the artist
+ */
+ public void setArtistProfileHeader(final Activity context,
+ final String artistName) {
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mSecondTab.setLabel(getResources().getString(R.string.page_albums));
+ mFirstTab.setArtistPhoto(context, artistName);
+ mSecondTab.setArtistAlbumPhoto(context, artistName);
+ mEnableSwipe = true;
+ }
+
+ /**
+ * Sets the album image header
+ *
+ * @param context The {@link Activity} to use
+ * @param albumName The key used to find the cached album art
+ * @param artistName The artist name used to find the cached artist image
+ */
+ public void setAlbumProfileHeader(final Activity context,
+ final String albumName, final String artistName) {
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mFirstTab.setAlbumPhoto(context, albumName, artistName);
+ mFirstTab.blurPhoto(context, artistName, albumName);
+ mSecondTab.setVisibility(View.GONE);
+ mEnableSwipe = false;
+ }
+
+ /**
+ * Sets the playlist or genre image header
+ *
+ * @param context The {@link Activity} to use
+ * @param profileName The key used to find the cached image for a playlist
+ * or genre
+ */
+ public void setPlaylistOrGenreProfileHeader(final Activity context,
+ final String profileName) {
+ mFirstTab.setDefault(context);
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mFirstTab.setPlaylistOrGenrePhoto(context, profileName);
+ mSecondTab.setVisibility(View.GONE);
+ mEnableSwipe = false;
+ }
+
+ /**
+ * Used to fetch for the album art via Last.fm.
+ *
+ * @param context The {@link Context} to use.
+ * @param albumName The name of the album in the profile the user is viewing.
+ * @param artistName The name of the album artist in the profile the user is viewing.
+ */
+ public void fetchAlbumPhoto(final Activity context, final String albumName,
+ final String artistName) {
+ mFirstTab.fetchAlbumPhoto(context, albumName, artistName);
+ }
+
+ /**
+ * @return The main {@link ImageView} for the first tab
+ */
+ public ImageView getHeaderPhoto() {
+ return mFirstTab.getPhoto();
+ }
+
+ /** When clicked, selects the corresponding tab. */
+ private final class TabClickListener implements OnClickListener {
+ private final int mTab;
+
+ public TabClickListener(final int tab) {
+ super();
+ mTab = tab;
+ }
+
+ @Override
+ public void onClick(final View v) {
+ mListener.onTabSelected(mTab);
+ }
+ }
+
+ /**
+ * Interface for callbacks invoked when the user interacts with the
+ * carousel.
+ */
+ public interface Listener {
+ public void onTouchDown();
+
+ public void onTouchUp();
+
+ public void onScrollChanged(int l, int t, int oldl, int oldt);
+
+ public void onTabSelected(int position);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/RepeatButton.java b/src/com/cyngn/eleven/widgets/RepeatButton.java
new file mode 100644
index 0000000..cecd769
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/RepeatButton.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.theme.HoloSelector;
+
+/**
+ * A custom {@link ImageButton} that represents the "repeat" button.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RepeatButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_ALL = "btn_playback_repeat_all";
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_CURRENT = "btn_playback_repeat_one";
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_NONE = "btn_playback_repeat";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public RepeatButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Set the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (cycle repeat modes)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.cycleRepeat();
+ updateRepeatState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for the repeat state.
+ */
+ public void updateRepeatState() {
+ switch (MusicUtils.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_ALL:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat_all));
+ setImageDrawable(mResources.getDrawable(REPEAT_ALL));
+ break;
+ case MusicPlaybackService.REPEAT_CURRENT:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat_one));
+ setImageDrawable(mResources.getDrawable(REPEAT_CURRENT));
+ break;
+ case MusicPlaybackService.REPEAT_NONE:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat));
+ setImageDrawable(mResources.getDrawable(REPEAT_NONE));
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/RepeatingImageButton.java b/src/com/cyngn/eleven/widgets/RepeatingImageButton.java
new file mode 100644
index 0000000..e633648
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/RepeatingImageButton.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.theme.HoloSelector;
+
+/**
+ * A {@link ImageButton} that will repeatedly call a 'listener' method as long
+ * as the button is pressed, otherwise functions like a typecal
+ * {@link ImageButton}
+ */
+public class RepeatingImageButton extends ImageButton implements OnClickListener {
+
+ /**
+ * Next button theme resource
+ */
+ private static final String NEXT = "btn_playback_next";
+
+ /**
+ * Previous button theme resource
+ */
+ private static final String PREVIOUS = "btn_playback_previous";
+
+ private static final long sInterval = 400;
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ private long mStartTime;
+
+ private int mRepeatCount;
+
+ private RepeatListener mListener;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public RepeatingImageButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ setFocusable(true);
+ setLongClickable(true);
+ setOnClickListener(this);
+ updateState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View view) {
+ switch (view.getId()) {
+ case R.id.action_button_previous:
+ MusicUtils.previous(getContext());
+ break;
+ case R.id.action_button_next:
+ MusicUtils.next();
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Sets the listener to be called while the button is pressed and the
+ * interval in milliseconds with which it will be called.
+ *
+ * @param l The listener that will be called
+ * @param interval The interval in milliseconds for calls
+ */
+ public void setRepeatListener(final RepeatListener l) {
+ mListener = l;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean performLongClick() {
+ if (mListener == null) {
+ ApolloUtils.showCheatSheet(this);
+ }
+ mStartTime = SystemClock.elapsedRealtime();
+ mRepeatCount = 0;
+ post(mRepeater);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ /* Remove the repeater, but call the hook one more time */
+ removeCallbacks(mRepeater);
+ if (mStartTime != 0) {
+ doRepeat(true);
+ mStartTime = 0;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onKeyDown(final int keyCode, final KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ /*
+ * Need to call super to make long press work, but return true
+ * so that the application doesn't get the down event
+ */
+ super.onKeyDown(keyCode, event);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onKeyUp(final int keyCode, final KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ /* Remove the repeater, but call the hook one more time */
+ removeCallbacks(mRepeater);
+ if (mStartTime != 0) {
+ doRepeat(true);
+ mStartTime = 0;
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private final Runnable mRepeater = new Runnable() {
+ @Override
+ public void run() {
+ doRepeat(false);
+ if (isPressed()) {
+ postDelayed(this, sInterval);
+ }
+ }
+ };
+
+ /**
+ * @param shouldRepeat If True the repeat count stops at -1, false if to add
+ * incrementally add the repeat count
+ */
+ private void doRepeat(final boolean shouldRepeat) {
+ final long now = SystemClock.elapsedRealtime();
+ if (mListener != null) {
+ mListener.onRepeat(this, now - mStartTime, shouldRepeat ? -1 : mRepeatCount++);
+ }
+ }
+
+ /**
+ * Sets the correct drawable for playback.
+ */
+ public void updateState() {
+ switch (getId()) {
+ case R.id.action_button_next:
+ setImageDrawable(mResources.getDrawable(NEXT));
+ break;
+ case R.id.action_button_previous:
+ setImageDrawable(mResources.getDrawable(PREVIOUS));
+ break;
+ default:
+ break;
+ }
+ }
+
+ public interface RepeatListener {
+
+ /**
+ * @param v View to be set
+ * @param duration Duration of the long press
+ * @param repeatcount The number of repeat counts
+ */
+ void onRepeat(View v, long duration, int repeatcount);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/SeparatedListAdapter.java b/src/com/cyngn/eleven/widgets/SeparatedListAdapter.java
new file mode 100644
index 0000000..0167941
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/SeparatedListAdapter.java
@@ -0,0 +1,157 @@
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+
+import com.cyngn.eleven.R;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SeparatedListAdapter extends BaseAdapter {
+
+ public final Map<String, Adapter> mSections = new LinkedHashMap<String, Adapter>();
+
+ public final ArrayAdapter<String> mHeaders;
+
+ public final static int TYPE_SECTION_HEADER = 0;
+
+ /**
+ * Constructor of <code>SeparatedListAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public SeparatedListAdapter(final Context context) {
+ mHeaders = new ArrayAdapter<String>(context, R.layout.list_header);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object getItem(int position) {
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return section;
+ }
+ if (position < size) {
+ return adapter.getItem(position - 1);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ // total together all mSections, plus one for each section header
+ int total = 0;
+ for (final Adapter adapter : mSections.values()) {
+ total += adapter.getCount() + 1;
+ }
+ return total;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ // assume that mHeaders count as one, then total all mSections
+ int total = 1;
+ for (final Adapter adapter : mSections.values()) {
+ total += adapter.getViewTypeCount();
+ }
+ return total;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int type = 1;
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return TYPE_SECTION_HEADER;
+ }
+ if (position < size) {
+ return type + adapter.getItemViewType(position - 1);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ type += adapter.getViewTypeCount();
+ }
+ return -1;
+ }
+
+ public boolean areAllItemsSelectable() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isEnabled(final int position) {
+ return getItemViewType(position) != TYPE_SECTION_HEADER;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(int position, final View convertView, final ViewGroup parent) {
+ int sectionnum = 0;
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return mHeaders.getView(sectionnum, convertView, parent);
+ }
+ if (position < size) {
+ return adapter.getView(position - 1, convertView, parent);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ sectionnum++;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ return position;
+ }
+
+ public void addSection(final String section, final Adapter adapter) {
+ mHeaders.add(section);
+ mSections.put(section, adapter);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/ShowHideMasterLayout.java b/src/com/cyngn/eleven/widgets/ShowHideMasterLayout.java
new file mode 100644
index 0000000..953c723
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ShowHideMasterLayout.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2012 Google Inc. Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+
+/**
+ * A layout that supports the Show/Hide pattern for portrait tablet layouts. See
+ * <a href=
+ * "http://developer.android.com/design/patterns/multi-pane-layouts.html#orientation"
+ * >Android Design &gt; Patterns &gt; Multi-pane Layouts & gt; Compound Views
+ * and Orientation Changes</a> for more details on this pattern. This layout
+ * should normally be used in association with the Up button. Specifically, show
+ * the master pane using {@link #showMaster(boolean, int)} when the Up button is
+ * pressed. If the master pane is visible, defer to normal Up behavior.
+ * <p>
+ * TODO: swiping should be more tactile and actually follow the user's finger.
+ * <p>
+ * Requires API level 11
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class ShowHideMasterLayout extends ViewGroup implements Animator.AnimatorListener {
+
+ /**
+ * A flag for {@link #showMaster(boolean, int)} indicating that the change
+ * in visiblity should not be animated.
+ */
+ public final static int FLAG_IMMEDIATE = 0x1;
+
+ private View sMasterView;
+
+ private View mDetailView;
+
+ private OnMasterVisibilityChangedListener mOnMasterVisibilityChangedListener;
+
+ private GestureDetector mGestureDetector;
+
+ private Runnable mShowMasterCompleteRunnable;
+
+ private boolean mFirstShow = true;
+
+ private boolean mMasterVisible = true;
+
+ private boolean mFlingToExposeMaster;
+
+ private boolean mIsAnimating;
+
+ /* The last measured master width, including its margins */
+ private int mTranslateAmount;
+
+ public interface OnMasterVisibilityChangedListener {
+ public void onMasterVisibilityChanged(boolean visible);
+ }
+
+ public ShowHideMasterLayout(final Context context) {
+ super(context);
+ init();
+ }
+
+ public ShowHideMasterLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ShowHideMasterLayout(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mGestureDetector = new GestureDetector(getContext(), mGestureListener);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(final AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(final LayoutParams p) {
+ return new MarginLayoutParams(p);
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int mCount = getChildCount();
+
+ /* Measure once to find the maximum child size */
+ int sMaxHeight = 0;
+ int sMaxWidth = 0;
+ int mChildState = 0;
+
+ for (int i = 0; i < mCount; i++) {
+ final View mChild = getChildAt(i);
+ if (mChild.getVisibility() == GONE) {
+ continue;
+ }
+
+ measureChildWithMargins(mChild, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final MarginLayoutParams mLayoutParams = (MarginLayoutParams)mChild.getLayoutParams();
+ sMaxWidth = Math.max(sMaxWidth, mChild.getMeasuredWidth() + mLayoutParams.leftMargin
+ + mLayoutParams.rightMargin);
+ sMaxHeight = Math.max(sMaxHeight, mChild.getMeasuredHeight() + mLayoutParams.topMargin
+ + mLayoutParams.bottomMargin);
+ mChildState = combineMeasuredStates(mChildState, mChild.getMeasuredState());
+ }
+
+ /* Account for padding too */
+ sMaxWidth += getPaddingLeft() + getPaddingRight();
+ sMaxHeight += getPaddingLeft() + getPaddingRight();
+
+ /* Check against our minimum height and width */
+ sMaxHeight = Math.max(sMaxHeight, getSuggestedMinimumHeight());
+ sMaxWidth = Math.max(sMaxWidth, getSuggestedMinimumWidth());
+
+ /* Set our own measured size */
+ setMeasuredDimension(
+ resolveSizeAndState(sMaxWidth, widthMeasureSpec, mChildState),
+ resolveSizeAndState(sMaxHeight, heightMeasureSpec,
+ mChildState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ /* Measure children for them to set their measured dimensions */
+ for (int i = 0; i < mCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final MarginLayoutParams mLayoutParams = (MarginLayoutParams)child.getLayoutParams();
+
+ int mChildWidthMeasureSpec;
+ int mChildHeightMeasureSpec;
+
+ if (mLayoutParams.width == LayoutParams.MATCH_PARENT) {
+ mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth()
+ - getPaddingLeft() - getPaddingRight() - mLayoutParams.leftMargin
+ - mLayoutParams.rightMargin, MeasureSpec.EXACTLY);
+ } else {
+ mChildWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft()
+ + getPaddingRight() + mLayoutParams.leftMargin + mLayoutParams.rightMargin,
+ mLayoutParams.width);
+ }
+
+ if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
+ mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight()
+ - getPaddingTop() - getPaddingBottom() - mLayoutParams.topMargin
+ - mLayoutParams.bottomMargin, MeasureSpec.EXACTLY);
+ } else {
+ mChildHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + mLayoutParams.topMargin
+ + mLayoutParams.bottomMargin, mLayoutParams.height);
+ }
+
+ child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r,
+ final int b) {
+ updateChildReferences();
+
+ if (sMasterView == null || mDetailView == null) {
+ return;
+ }
+
+ final int sMasterWidth = sMasterView.getMeasuredWidth();
+ final MarginLayoutParams sMasterLp = (MarginLayoutParams)sMasterView.getLayoutParams();
+ final MarginLayoutParams mDetailLp = (MarginLayoutParams)mDetailView.getLayoutParams();
+
+ mTranslateAmount = sMasterWidth + sMasterLp.leftMargin + sMasterLp.rightMargin;
+
+ sMasterView.layout(l + sMasterLp.leftMargin, t + sMasterLp.topMargin, l
+ + sMasterLp.leftMargin + sMasterWidth, b - sMasterLp.bottomMargin);
+
+ mDetailView.layout(l + mDetailLp.leftMargin + mTranslateAmount, t + mDetailLp.topMargin, r
+ - mDetailLp.rightMargin + mTranslateAmount, b - mDetailLp.bottomMargin);
+
+ /* Update translationX values */
+ if (!mIsAnimating) {
+ final float mTranslationX = mMasterVisible ? 0 : -mTranslateAmount;
+ sMasterView.setTranslationX(mTranslationX);
+ mDetailView.setTranslationX(mTranslationX);
+ }
+ }
+
+ private void updateChildReferences() {
+ final int mChildCount = getChildCount();
+ sMasterView = mChildCount > 0 ? getChildAt(0) : null;
+ mDetailView = mChildCount > 1 ? getChildAt(1) : null;
+ }
+
+ /**
+ * Allow or disallow the user to flick right on the detail pane to expose
+ * the master pane.
+ *
+ * @param enabled Whether or not to enable this interaction.
+ */
+ public void setFlingToExposeMasterEnabled(final boolean enabled) {
+ mFlingToExposeMaster = enabled;
+ }
+
+ /**
+ * Request the given listener be notified when the master pane is shown or
+ * hidden.
+ *
+ * @param listener The listener to notify when the master pane is shown or
+ * hidden.
+ */
+ public void setOnMasterVisibilityChangedListener(
+ final OnMasterVisibilityChangedListener listener) {
+ mOnMasterVisibilityChangedListener = listener;
+ }
+
+ /**
+ * Returns whether or not the master pane is visible.
+ *
+ * @return True if the master pane is visible.
+ */
+ public boolean isMasterVisible() {
+ return mMasterVisible;
+ }
+
+ /**
+ * Calls {@link #showMaster(boolean, int, Runnable)} with a null runnable.
+ */
+ public void showMaster(final boolean show, final int flags) {
+ showMaster(show, flags, null);
+ }
+
+ /**
+ * Shows or hides the master pane.
+ *
+ * @param show Whether or not to show the master pane.
+ * @param flags {@link #FLAG_IMMEDIATE} to show/hide immediately, or 0 to
+ * animate.
+ * @param completeRunnable An optional runnable to run when any animations
+ * related to this are complete.
+ */
+ public void showMaster(final boolean show, final int flags, final Runnable completeRunnable) {
+ if (!mFirstShow && mMasterVisible == show) {
+ return;
+ }
+
+ mShowMasterCompleteRunnable = completeRunnable;
+ mFirstShow = false;
+
+ mMasterVisible = show;
+ if (mOnMasterVisibilityChangedListener != null) {
+ mOnMasterVisibilityChangedListener.onMasterVisibilityChanged(show);
+ }
+
+ updateChildReferences();
+
+ if (sMasterView == null || mDetailView == null) {
+ return;
+ }
+
+ final float mTranslationX = show ? 0 : -mTranslateAmount;
+
+ if ((flags & FLAG_IMMEDIATE) != 0) {
+ sMasterView.setTranslationX(mTranslationX);
+ mDetailView.setTranslationX(mTranslationX);
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ } else {
+ final long mDuration = getResources()
+ .getInteger(android.R.integer.config_shortAnimTime);
+
+ /* Animate if we have Honeycomb APIs, don't animate otherwise */
+ mIsAnimating = true;
+ final AnimatorSet mAnimatorSet = new AnimatorSet();
+ sMasterView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ mDetailView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ mAnimatorSet.play(
+ ObjectAnimator.ofFloat(sMasterView, "translationX", mTranslationX).setDuration(
+ mDuration)).with(
+ ObjectAnimator.ofFloat(mDetailView, "translationX", mTranslationX).setDuration(
+ mDuration));
+ mAnimatorSet.addListener(this);
+ mAnimatorSet.start();
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
+ // Really bad hack... we really shouldn't do this.
+ // super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent event) {
+ if (mFlingToExposeMaster && !mMasterVisible) {
+ mGestureDetector.onTouchEvent(event);
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
+ if (event.getX() > mTranslateAmount) {
+ return true;
+ }
+ }
+ return super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (mFlingToExposeMaster && !mMasterVisible && mGestureDetector.onTouchEvent(event)) {
+ return true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
+ if (event.getX() > mTranslateAmount) {
+ showMaster(false, 0);
+ return true;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animator) {
+ mIsAnimating = false;
+ sMasterView.setLayerType(LAYER_TYPE_NONE, null);
+ mDetailView.setLayerType(LAYER_TYPE_NONE, null);
+ requestLayout();
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(final Animator animator) {
+ mIsAnimating = false;
+ sMasterView.setLayerType(LAYER_TYPE_NONE, null);
+ mDetailView.setLayerType(LAYER_TYPE_NONE, null);
+ requestLayout();
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ }
+
+ private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(final MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
+ final float velocityY) {
+ final ViewConfiguration mViewConfig = ViewConfiguration.get(getContext());
+ final float mAbsVelocityX = Math.abs(velocityX);
+ final float mAbsVelocityY = Math.abs(velocityY);
+ if (mFlingToExposeMaster && !mMasterVisible && velocityX > 0
+ && mAbsVelocityX >= mAbsVelocityY
+ && mAbsVelocityX > mViewConfig.getScaledMinimumFlingVelocity()
+ && mAbsVelocityX < mViewConfig.getScaledMaximumFlingVelocity()) {
+ showMaster(true, 0);
+ return true;
+ }
+ return super.onFling(e1, e2, velocityX, velocityY);
+ }
+ };
+
+ @Override
+ public void onAnimationStart(final Animator animator) {
+ /* Nothing to do */
+ }
+
+ @Override
+ public void onAnimationRepeat(final Animator animator) {
+ /* Nothing to do */
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/ShuffleButton.java b/src/com/cyngn/eleven/widgets/ShuffleButton.java
new file mode 100644
index 0000000..808656b
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/ShuffleButton.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.cyngn.eleven.MusicPlaybackService;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.cyngn.eleven.widgets.theme.HoloSelector;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ShuffleButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Shuffle theme resource
+ */
+ private static final String SHUFFLE = "btn_playback_shuffle";
+
+ /**
+ * Shuffle all theme resource
+ */
+ private static final String SHUFFLE_ALL = "btn_playback_shuffle_all";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ShuffleButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (cycle shuffle)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.cycleShuffle();
+ updateShuffleState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for the shuffle state.
+ */
+ public void updateShuffleState() {
+ switch (MusicUtils.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NORMAL:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle_all));
+ setImageDrawable(mResources.getDrawable(SHUFFLE_ALL));
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle_all));
+ setImageDrawable(mResources.getDrawable(SHUFFLE_ALL));
+ break;
+ case MusicPlaybackService.SHUFFLE_NONE:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle));
+ setImageDrawable(mResources.getDrawable(SHUFFLE));
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/SquareImageView.java b/src/com/cyngn/eleven/widgets/SquareImageView.java
new file mode 100644
index 0000000..31eeaac
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/SquareImageView.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A custom {@link ImageView} that is sized to be a perfect square, otherwise
+ * functions like a typical {@link ImageView}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SquareImageView extends LayoutSuppressingImageView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public SquareImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMeasure(final int widthSpec, final int heightSpec) {
+ super.onMeasure(widthSpec, heightSpec);
+ final int mSize = Math.min(getMeasuredWidth(), getMeasuredHeight());
+ setMeasuredDimension(mSize, mSize);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/SquareView.java b/src/com/cyngn/eleven/widgets/SquareView.java
new file mode 100644
index 0000000..27bfa89
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/SquareView.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A custom {@link ViewGroup} used to make it's children into perfect squares.
+ * Useful when dealing with grid images and especially album art.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SquareView extends ViewGroup {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public SquareView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final View mChildren = getChildAt(0);
+ mChildren.measure(widthMeasureSpec, widthMeasureSpec);
+ final int mWidth = resolveSize(mChildren.getMeasuredWidth(), widthMeasureSpec);
+ mChildren.measure(mWidth, mWidth);
+ setMeasuredDimension(mWidth, mWidth);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int u, final int r,
+ final int d) {
+ getChildAt(0).layout(0, 0, r - l, d - u);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/VerticalScrollListener.java b/src/com/cyngn/eleven/widgets/VerticalScrollListener.java
new file mode 100644
index 0000000..f126b75
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/VerticalScrollListener.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets;
+
+import android.annotation.SuppressLint;
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+
+import com.cyngn.eleven.utils.ApolloUtils;
+
+@SuppressLint("NewApi")
+public class VerticalScrollListener implements OnScrollListener {
+
+ /* Used to determine the off set to scroll the header */
+ private final ScrollableHeader mHeader;
+
+ private final ProfileTabCarousel mTabCarousel;
+
+ private final int mPageIndex;
+
+ public VerticalScrollListener(final ScrollableHeader header, final ProfileTabCarousel carousel,
+ final int pageIndex) {
+ mHeader = header;
+ mTabCarousel = carousel;
+ mPageIndex = pageIndex;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+
+ if (mTabCarousel == null || mTabCarousel.isTabCarouselIsAnimating()) {
+ return;
+ }
+
+ final View top = view.getChildAt(firstVisibleItem);
+ if (top == null) {
+ return;
+ }
+
+ if (firstVisibleItem != 0) {
+ mTabCarousel.moveToYCoordinate(mPageIndex,
+ -mTabCarousel.getAllowedVerticalScrollLength());
+ return;
+ }
+
+ float y = view.getChildAt(firstVisibleItem).getY();
+ final float amtToScroll = Math.max(y, -mTabCarousel.getAllowedVerticalScrollLength());
+ mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ if (mHeader != null) {
+ mHeader.onScrollStateChanged(view, scrollState);
+ }
+ }
+
+ /** Defines the header to be scrolled. */
+ public interface ScrollableHeader {
+
+ /* Used the pause the disk cache while scrolling */
+ public void onScrollStateChanged(AbsListView view, int scrollState);
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/BottomActionBar.java b/src/com/cyngn/eleven/widgets/theme/BottomActionBar.java
new file mode 100644
index 0000000..2b9cbf1
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/BottomActionBar.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ThemeUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressWarnings("deprecation")
+public class BottomActionBar extends RelativeLayout {
+
+ /**
+ * Resource name used to theme the bottom action bar
+ */
+ private static final String BOTTOM_ACTION_BAR = "bottom_action_bar";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @throws NameNotFoundException
+ */
+ public BottomActionBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the bottom action bar
+ setBackgroundDrawable(resources.getDrawable(BOTTOM_ACTION_BAR));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ // Theme the selector
+ final LinearLayout bottomActionBar = (LinearLayout)findViewById(R.id.bottom_action_bar);
+ bottomActionBar.setBackgroundDrawable(new HoloSelector(getContext()));
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/Colorstrip.java b/src/com/cyngn/eleven/widgets/theme/Colorstrip.java
new file mode 100644
index 0000000..b613bec
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/Colorstrip.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.cyngn.eleven.utils.ThemeUtils;
+
+/**
+ * Used as a thin strip placed just above the bottom action bar or just below
+ * the top action bar.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Colorstrip extends View {
+
+ /**
+ * Resource name used to theme the colorstrip
+ */
+ private static final String COLORSTRIP = "colorstrip";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public Colorstrip(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the colorstrip
+ setBackgroundColor(resources.getColor(COLORSTRIP));
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/HoloSelector.java b/src/com/cyngn/eleven/widgets/theme/HoloSelector.java
new file mode 100644
index 0000000..9f76ee7
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/HoloSelector.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.StateListDrawable;
+
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.ThemeUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A themeable {@link StateListDrawable}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class HoloSelector extends StateListDrawable {
+
+ /**
+ * Used to theme the touched and focused colors
+ */
+ private static final String RESOURCE_NAME = "holo_selector";
+
+ /**
+ * Focused state
+ */
+ private static final int FOCUSED = android.R.attr.state_focused;
+
+ /**
+ * Pressed state
+ */
+ private static final int PRESSED = android.R.attr.state_pressed;
+
+ /**
+ * Constructor for <code>HoloSelector</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ @SuppressLint("NewApi")
+ public HoloSelector(final Context context) {
+ final ThemeUtils resources = new ThemeUtils(context);
+ final int themeColor = resources.getColor(RESOURCE_NAME);
+ // Focused
+ addState(new int[] {
+ FOCUSED
+ }, makeColorDrawable(themeColor));
+ // Pressed
+ addState(new int[] {
+ PRESSED
+ }, makeColorDrawable(themeColor));
+ // Default
+ addState(new int[] {}, makeColorDrawable(Color.TRANSPARENT));
+ setExitFadeDuration(400);
+ }
+
+ /**
+ * @param color The color to use.
+ * @return A new {@link ColorDrawable}.
+ */
+ private static final ColorDrawable makeColorDrawable(final int color) {
+ return new WeakReference<ColorDrawable>(new ColorDrawable(color)).get();
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/ThemeableFrameLayout.java b/src/com/cyngn/eleven/widgets/theme/ThemeableFrameLayout.java
new file mode 100644
index 0000000..e747989
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/ThemeableFrameLayout.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.cyngn.eleven.utils.ThemeUtils;
+
+/**
+ * This is a custom {@link FrameLayout} that is used as the main conent when
+ * transacting fragments that is made themeable by allowing developers to change
+ * the background.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableFrameLayout extends FrameLayout {
+
+ /**
+ * Used to set the background
+ */
+ public static final String BACKGROUND = "pager_background";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ThemeableFrameLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the layout
+ setBackgroundDrawable(resources.getDrawable(BACKGROUND));
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/ThemeableSeekBar.java b/src/com/cyngn/eleven/widgets/theme/ThemeableSeekBar.java
new file mode 100644
index 0000000..1251d4e
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/ThemeableSeekBar.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+import com.cyngn.eleven.utils.ThemeUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableSeekBar extends SeekBar {
+
+ /**
+ * Used to set the progess bar
+ */
+ public static final String PROGESS = "audio_player_seekbar";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ThemeableSeekBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the seek bar
+ setProgressDrawable(resources.getDrawable(PROGESS));
+ }
+
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/ThemeableTextView.java b/src/com/cyngn/eleven/widgets/theme/ThemeableTextView.java
new file mode 100644
index 0000000..9cde3da
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/ThemeableTextView.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.utils.ThemeUtils;
+
+import java.util.WeakHashMap;
+
+/**
+ * A custom {@link TextView} that is made themeable for developers. It allows a
+ * custom font and color to be set, otherwise functions like normal. Because
+ * different text views may required different colors to be set, the resource
+ * name each can be set in the XML via the attribute {@value themeResource}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableTextView extends TextView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ThemeableTextView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Retrieve styles attributes
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+ R.styleable.ThemeableTextView, 0, 0);
+ // Get the theme resource name
+ final String resourceName = typedArray
+ .getString(R.styleable.ThemeableTextView_themeResource);
+ // Theme the text color
+ if (!TextUtils.isEmpty(resourceName)) {
+ setTextColor(resources.getColor(resourceName));
+ }
+ // Recyle the attrs
+ typedArray.recycle();
+ }
+
+ /**
+ * A small class that holds a weak cache for any typefaces applied to the
+ * text.
+ */
+ public static final class TypefaceCache {
+
+ private static final WeakHashMap<String, Typeface> MAP = new WeakHashMap<String, Typeface>();
+
+ private static TypefaceCache sInstance;
+
+ /**
+ * Constructor for <code>TypefaceCache</code>
+ */
+ public TypefaceCache() {
+ }
+
+ /**
+ * @return A singleton of {@linkTypefaceCache}.
+ */
+ public static final TypefaceCache getInstance() {
+ if (sInstance == null) {
+ sInstance = new TypefaceCache();
+ }
+ return sInstance;
+ }
+
+ /**
+ * @param file The name of the type face asset.
+ * @param context The {@link Context} to use.
+ * @return A new type face.
+ */
+ public Typeface getTypeface(final String file, final Context context) {
+ Typeface result = MAP.get(file);
+ if (result == null) {
+ result = Typeface.createFromAsset(context.getAssets(), file);
+ MAP.put(file, result);
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/widgets/theme/ThemeableTitlePageIndicator.java b/src/com/cyngn/eleven/widgets/theme/ThemeableTitlePageIndicator.java
new file mode 100644
index 0000000..2455c34
--- /dev/null
+++ b/src/com/cyngn/eleven/widgets/theme/ThemeableTitlePageIndicator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyngn.eleven.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.cyngn.eleven.utils.ThemeUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+/**
+ * This is a custom {@link TitlePageIndicator} that is made themeable by
+ * allowing developers to choose the background and the selected and unselected
+ * text colors.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableTitlePageIndicator extends TitlePageIndicator {
+
+ /**
+ * Resource name used to theme the background
+ */
+ private static final String BACKGROUND = "tpi_background";
+
+ /**
+ * Resource name used to theme the selected text color
+ */
+ private static final String SELECTED_TEXT = "tpi_selected_text_color";
+
+ /**
+ * Resource name used to theme the unselected text color
+ */
+ private static final String TEXT = "tpi_unselected_text_color";
+
+ /**
+ * Resource name used to theme the footer color
+ */
+ private static final String FOOTER = "tpi_footer_color";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ThemeableTitlePageIndicator(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the background
+ setBackgroundDrawable(resources.getDrawable(BACKGROUND));
+ // Theme the selected text color
+ setSelectedColor(resources.getColor(SELECTED_TEXT));
+ // Theme the unselected text color
+ setTextColor(resources.getColor(TEXT));
+ // Theme the footer
+ setFooterColor(resources.getColor(FOOTER));
+ }
+}