diff options
author | Linus Lee <llee@cyngn.com> | 2014-08-04 15:05:06 -0700 |
---|---|---|
committer | Linus Lee <llee@cyngn.com> | 2014-08-04 15:05:06 -0700 |
commit | 5601e2a4123fa540ecb6fb484ea0547338c5cf26 (patch) | |
tree | 9237c4d3fd998e7032bf81e58aaa15fff080af4d /src/com/cyngn/eleven | |
parent | 3b7f423ddabc26ff0430e5ff9f446fb7aeaf578b (diff) | |
download | android_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')
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 > Patterns > 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)); + } +} |