Playlist editor
This commit is contained in:
parent
712eee13b5
commit
99b92f7e14
@ -45,6 +45,10 @@ THE SOFTWARE.
|
||||
android:name="LibraryActivity"
|
||||
android:theme="@style/NoTitle"
|
||||
android:launchMode="singleTop" />
|
||||
<activity
|
||||
android:name="PlaylistActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Black" />
|
||||
<activity
|
||||
android:name="MiniPlaybackActivity"
|
||||
android:theme="@style/Dialog"
|
||||
|
BIN
res/drawable-hdpi/grabber.png
Normal file
BIN
res/drawable-hdpi/grabber.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-mdpi/grabber.png
Normal file
BIN
res/drawable-mdpi/grabber.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 550 B |
@ -110,7 +110,8 @@ THE SOFTWARE.
|
||||
<FrameLayout
|
||||
android:id="@+id/lists"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
android:layout_height="0px"
|
||||
android:layout_weight="1">
|
||||
<ListView
|
||||
android:id="@+id/artist_list"
|
||||
android:visibility="gone"
|
||||
|
28
res/layout/playlist_activity.xml
Normal file
28
res/layout/playlist_activity.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<org.kreed.vanilla.DragListView
|
||||
android:id="@+id/playlist"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
</merge>
|
41
res/layout/playlist_buttons.xml
Normal file
41
res/layout/playlist_buttons.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:padding="3dip"
|
||||
android:orientation="horizontal">
|
||||
<Button
|
||||
android:id="@+id/edit"
|
||||
android:layout_width="0px"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/edit" />
|
||||
<Button
|
||||
android:id="@+id/delete"
|
||||
android:layout_width="0px"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/delete" />
|
||||
</LinearLayout>
|
@ -21,8 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Dialog" parent="android:style/Theme.DeviceDefault.Dialog" />
|
||||
<style name="NoTitle" parent="android:style/Theme.DeviceDefault">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
<style name="Dialog" parent="android:Theme.DeviceDefault.Dialog" />
|
||||
<style name="Theme" parent="android:Theme.DeviceDefault" />
|
||||
</resources>
|
||||
|
@ -56,6 +56,7 @@ THE SOFTWARE.
|
||||
<string name="delete">Delete</string>
|
||||
<string name="playback_view">Now Playing</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="done">Done</string>
|
||||
|
||||
<plurals name="playing">
|
||||
<item quantity="one">1 song playing.</item>
|
||||
|
@ -21,11 +21,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Dialog" parent="android:style/Theme.Dialog" />
|
||||
<style name="NoTitle" parent="android:style/Theme">
|
||||
<style name="Dialog" parent="android:Theme.Dialog" />
|
||||
<style name="Theme" parent="android:Theme" />
|
||||
<style name="Black" parent="Theme">
|
||||
<item name="android:colorBackground">@android:color/black</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
<style name="NoTitle" parent="Theme">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
<style name="NoBackground" parent="style/NoTitle">
|
||||
<style name="NoBackground" parent="Theme">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
</style>
|
||||
<string-array name="entry_values">
|
||||
|
369
src/org/kreed/vanilla/DragListView.java
Normal file
369
src/org/kreed/vanilla/DragListView.java
Normal file
@ -0,0 +1,369 @@
|
||||
/*
|
||||
* 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 org.kreed.vanilla;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* A ListView that supports dragging to reorder its elements.
|
||||
*
|
||||
* This implementation has some restrictions:
|
||||
* Footers are unsupported
|
||||
* All non-header views must be MediaViews of uniform height
|
||||
* The adapter must be a PlaylistAdapter
|
||||
*
|
||||
* Dragging disabled by default. Enable it with
|
||||
* {@link DragListView#setEditable(boolean)}.
|
||||
*
|
||||
* This should really be built-in to Android. This implementation is SUPER-
|
||||
* HACKY. : /
|
||||
*/
|
||||
public class DragListView extends ListView implements Handler.Callback {
|
||||
private static final int MSG_SCROLL = 0;
|
||||
|
||||
private final Handler mHandler = new Handler(this);
|
||||
private PlaylistAdapter mAdapter;
|
||||
|
||||
/**
|
||||
* True to allow dragging; false otherwise.
|
||||
*/
|
||||
private boolean mEditable;
|
||||
|
||||
private ImageView mDragView;
|
||||
private Bitmap mDragBitmap;
|
||||
private WindowManager mWindowManager;
|
||||
private WindowManager.LayoutParams mWindowParams;
|
||||
/**
|
||||
* At which position is the item currently being dragged. Note that this
|
||||
* takes in to account header items.
|
||||
*/
|
||||
private int mDragPos;
|
||||
/**
|
||||
* At which position was the item being dragged originally
|
||||
*/
|
||||
private int mSrcDragPos;
|
||||
/**
|
||||
* At what y offset inside the dragged view did the user grab it.
|
||||
*/
|
||||
private int mDragPointY;
|
||||
/**
|
||||
* The difference between screen coordinates and coordinates in the drag
|
||||
* view.
|
||||
*/
|
||||
private int mYOffset;
|
||||
/**
|
||||
* The height of the view being dragged.
|
||||
*/
|
||||
private int mDragHeight;
|
||||
/**
|
||||
* The y coordinate of the top of the drag view after the last motion
|
||||
* event.
|
||||
*/
|
||||
private int mLastMotionY;
|
||||
|
||||
public DragListView(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setAdapter(PlaylistAdapter adapter)
|
||||
{
|
||||
super.setAdapter(adapter);
|
||||
// Keep track of adapter here since getAdapter() will return a wrapper
|
||||
// when there are headers.
|
||||
mAdapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to allow elements to be reordered.
|
||||
*
|
||||
* @param editable True to allow reordering.
|
||||
*/
|
||||
public void setEditable(boolean editable)
|
||||
{
|
||||
mEditable = editable;
|
||||
if (!editable)
|
||||
stopDragging();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev)
|
||||
{
|
||||
if (mEditable) {
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
stopDragging();
|
||||
|
||||
int x = (int)ev.getX();
|
||||
// The left half of the item is the grabber for dragging the item
|
||||
if (x < getWidth() / 4) {
|
||||
int item = pointToPosition(x, (int)ev.getY());
|
||||
if (item != AdapterView.INVALID_POSITION && item >= getHeaderViewsCount()) {
|
||||
startDragging(item, ev);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev)
|
||||
{
|
||||
if (!mEditable || mDragView == null)
|
||||
return super.onTouchEvent(ev);
|
||||
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
stopDragging();
|
||||
int offset = getHeaderViewsCount();
|
||||
if (mDragPos >= offset && mDragPos < getCount())
|
||||
mAdapter.move(mSrcDragPos - offset, mDragPos - offset);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
int y = (int)ev.getY() - mDragPointY;
|
||||
mLastMotionY = y;
|
||||
mWindowParams.x = 0;
|
||||
mWindowParams.y = y + mYOffset;
|
||||
mWindowManager.updateViewLayout(mDragView, mWindowParams);
|
||||
computeDragPosition(y);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore size and visibility for all list items
|
||||
*/
|
||||
private void unExpandViews()
|
||||
{
|
||||
for (int i = 0, count = getChildCount(); i != count; ++i) {
|
||||
View view = getChildAt(i);
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.height = 0;
|
||||
view.setLayoutParams(params);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust visibility and size to make it appear as though
|
||||
* an item is being dragged around and other items are making
|
||||
* room for it.
|
||||
*
|
||||
* If dropping the item would result in it still being in the
|
||||
* same place, then make the dragged list item's size normal,
|
||||
* but make the item invisible.
|
||||
* Otherwise, if the dragged list item is still on screen, make
|
||||
* it as small as possible and expand the item below the insert
|
||||
* point.
|
||||
*/
|
||||
private void doExpansion()
|
||||
{
|
||||
int firstVisibile = getFirstVisiblePosition();
|
||||
int childNum = mDragPos - firstVisibile;
|
||||
if (mDragPos > mSrcDragPos)
|
||||
childNum += 1;
|
||||
|
||||
int headerCount = getHeaderViewsCount();
|
||||
int childCount = getChildCount();
|
||||
|
||||
View dragSrcView = getChildAt(mSrcDragPos - firstVisibile);
|
||||
|
||||
int start = firstVisibile < headerCount ? headerCount - firstVisibile : 0;
|
||||
for (int i = start; i != childCount; ++i) {
|
||||
MediaView view = (MediaView)getChildAt(i);
|
||||
|
||||
int height = 0;
|
||||
int visibility = View.VISIBLE;
|
||||
boolean gravity = true;
|
||||
|
||||
if (view == dragSrcView) {
|
||||
if (mDragPos == mSrcDragPos) {
|
||||
// hovering over the original location: show empty space
|
||||
visibility = View.INVISIBLE;
|
||||
} else {
|
||||
// not hovering over it: show nothing
|
||||
// Ideally the item would be completely gone, but neither
|
||||
// setting its size to 0 nor settings visibility to GONE
|
||||
// has the desired effect.
|
||||
height = 1;
|
||||
}
|
||||
} else if (i == childNum) {
|
||||
// hovering over this row; expand it to "make room" for the
|
||||
// dragged item
|
||||
height = view.getPreferredHeight() * 2;
|
||||
} else if (childNum == childCount && i == childCount - 1) {
|
||||
// hovering over the bottom of the list: we need to show the
|
||||
// expanded area at the bottom of the view, so set the gravity
|
||||
// to top
|
||||
height = view.getPreferredHeight() * 2;
|
||||
gravity = false;
|
||||
}
|
||||
|
||||
view.setBottomGravity(gravity);
|
||||
view.setVisibility(visibility);
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.height = height;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the drag position based on where the drag view is hovering.
|
||||
* Expands views and updates scrolling when this position changes.
|
||||
*
|
||||
* @param y The y coordinate of the top of the drag view.
|
||||
* @return The scrolling speed in pixels
|
||||
*/
|
||||
private int computeDragPosition(int y)
|
||||
{
|
||||
// This assumes uniform height for all non-header rows
|
||||
int firstVisible = getFirstVisiblePosition();
|
||||
int topPos = Math.max(getHeaderViewsCount(), firstVisible);
|
||||
int dragHeight = mDragHeight;
|
||||
View view = getChildAt(topPos - firstVisible);
|
||||
int viewMiddle = view.getTop() + dragHeight / 2;
|
||||
int dragPos = Math.min(getCount() - 1, topPos + Math.max(0, y - viewMiddle + dragHeight) / dragHeight);
|
||||
|
||||
if (dragPos != mDragPos) {
|
||||
mDragPos = dragPos;
|
||||
doExpansion();
|
||||
}
|
||||
|
||||
int height = getHeight();
|
||||
int upperBound = height / 4;
|
||||
int lowerBound = height * 3 / 4;
|
||||
|
||||
if (y > lowerBound && (getLastVisiblePosition() < getCount() - 1 || getChildAt(getChildCount() - 1).getBottom() > getBottom()))
|
||||
return y > (height + lowerBound) / 2 ? 16 : 4;
|
||||
else if (y < upperBound && (getFirstVisiblePosition() != 0 || getChildAt(0).getTop() < 0))
|
||||
return y < upperBound / 2 ? -16 : -4;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a drag operation.
|
||||
*
|
||||
* @param row The row number of the item to drag
|
||||
* @param ev The touch event that started this drag.
|
||||
*/
|
||||
private void startDragging(int row, MotionEvent ev)
|
||||
{
|
||||
int y = (int)ev.getY();
|
||||
|
||||
View item = getChildAt(row - getFirstVisiblePosition());
|
||||
mDragPointY = y - item.getTop();
|
||||
mYOffset = (int)ev.getRawY() - y;
|
||||
|
||||
mWindowParams = new WindowManager.LayoutParams();
|
||||
mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
mWindowParams.x = 0;
|
||||
mWindowParams.y = y - mDragPointY + mYOffset;
|
||||
|
||||
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
|
||||
mWindowParams.format = PixelFormat.TRANSLUCENT;
|
||||
mWindowParams.windowAnimations = 0;
|
||||
|
||||
item.buildDrawingCache();
|
||||
// Create a copy of the drawing cache so that it does not get recycled
|
||||
// by the framework when the list tries to clean up memory
|
||||
Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache());
|
||||
item.destroyDrawingCache();
|
||||
mDragBitmap = bitmap;
|
||||
|
||||
Context context = getContext();
|
||||
ImageView view = new ImageView(context);
|
||||
view.setPadding(0, 0, 0, 0);
|
||||
view.setImageBitmap(bitmap);
|
||||
|
||||
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
|
||||
mWindowManager.addView(view, mWindowParams);
|
||||
mDragView = view;
|
||||
|
||||
mDragHeight = bitmap.getHeight();
|
||||
mSrcDragPos = row;
|
||||
// Force expansion on next motion event
|
||||
mDragPos = INVALID_POSITION;
|
||||
|
||||
mHandler.sendEmptyMessageDelayed(MSG_SCROLL, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a drag operation.
|
||||
*/
|
||||
private void stopDragging()
|
||||
{
|
||||
if (mDragView != null) {
|
||||
mDragView.setVisibility(GONE);
|
||||
mWindowManager.removeView(mDragView);
|
||||
mDragView.setImageDrawable(null);
|
||||
mDragView = null;
|
||||
}
|
||||
if (mDragBitmap != null) {
|
||||
mDragBitmap.recycle();
|
||||
mDragBitmap = null;
|
||||
}
|
||||
unExpandViews();
|
||||
mHandler.removeMessages(MSG_SCROLL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message message)
|
||||
{
|
||||
if (message.what == MSG_SCROLL) {
|
||||
if (mDragPos != INVALID_POSITION) {
|
||||
int speed = computeDragPosition(mLastMotionY);
|
||||
if (speed != 0) {
|
||||
View view = getChildAt(0);
|
||||
if (view != null) {
|
||||
int pos = view.getTop();
|
||||
setSelectionFromTop(getFirstVisiblePosition(), pos - speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
mHandler.sendEmptyMessageDelayed(MSG_SCROLL, 50);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -140,7 +140,7 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
mArtistAdapter = setupView(R.id.artist_list, MediaUtils.TYPE_ARTIST, true, true, null);
|
||||
mAlbumAdapter = setupView(R.id.album_list, MediaUtils.TYPE_ALBUM, true, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_albums"));
|
||||
mSongAdapter = setupView(R.id.song_list, MediaUtils.TYPE_SONG, false, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_songs"));
|
||||
mPlaylistAdapter = setupView(R.id.playlist_list, MediaUtils.TYPE_PLAYLIST, false, false, null);
|
||||
mPlaylistAdapter = setupView(R.id.playlist_list, MediaUtils.TYPE_PLAYLIST, true, false, null);
|
||||
mGenreAdapter = setupView(R.id.genre_list, MediaUtils.TYPE_GENRE, true, false, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_genres"));
|
||||
// These should be in the same order as MediaUtils.TYPE_*
|
||||
mAdapters = new MediaAdapter[] { mArtistAdapter, mAlbumAdapter, mSongAdapter, mPlaylistAdapter, mGenreAdapter };
|
||||
@ -267,7 +267,7 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
|
||||
int mode = modeForAction[action];
|
||||
QueryTask query = buildQueryFromIntent(intent, false);
|
||||
PlaybackService.get(this).addSongs(mode, query);
|
||||
PlaybackService.get(this).addSongs(mode, query, 0);
|
||||
|
||||
mLastActedId = intent.getLongExtra("id", -1);
|
||||
|
||||
@ -334,12 +334,17 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
public void onItemClick(AdapterView<?> list, View view, int pos, long id)
|
||||
{
|
||||
MediaView mediaView = (MediaView)view;
|
||||
if (mediaView.isExpanderPressed())
|
||||
expand(createClickIntent((MediaAdapter)list.getAdapter(), mediaView));
|
||||
else if (id == mLastActedId)
|
||||
MediaAdapter adapter = (MediaAdapter)list.getAdapter();
|
||||
if (mediaView.isRightBitmapPressed()) {
|
||||
if (adapter == mPlaylistAdapter)
|
||||
editPlaylist(mediaView.getMediaId(), mediaView.getTitle());
|
||||
else
|
||||
expand(createClickIntent(adapter, mediaView));
|
||||
} else if (id == mLastActedId) {
|
||||
startActivity(new Intent(this, FullPlaybackActivity.class));
|
||||
else
|
||||
pickSongs(createClickIntent((MediaAdapter)list.getAdapter(), mediaView), mDefaultAction);
|
||||
} else {
|
||||
pickSongs(createClickIntent(adapter, mediaView), mDefaultAction);
|
||||
}
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable editable)
|
||||
@ -450,7 +455,6 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("type", adapter.getMediaType());
|
||||
intent.putExtra("id", view.getMediaId());
|
||||
intent.putExtra("isHeader", view.isHeader());
|
||||
intent.putExtra("title", view.getTitle());
|
||||
return intent;
|
||||
}
|
||||
@ -472,13 +476,12 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
else
|
||||
projection = empty ? Song.EMPTY_PROJECTION : Song.FILLED_PROJECTION;
|
||||
|
||||
long id = intent.getLongExtra("id", -1);
|
||||
QueryTask query;
|
||||
if (intent.getBooleanExtra("isHeader", false)) {
|
||||
if (id == MediaView.HEADER_ID)
|
||||
query = mAdapters[type - 1].buildSongQuery(projection);
|
||||
} else {
|
||||
long id = intent.getLongExtra("id", -1);
|
||||
else
|
||||
query = MediaUtils.buildQuery(type, id, projection, null);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
@ -503,7 +506,9 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
// as worked is performed in the background.
|
||||
Intent intent = createClickIntent(adapter, view);
|
||||
|
||||
if (view.isHeader())
|
||||
boolean isHeader = view.getMediaId() == MediaView.HEADER_ID;
|
||||
|
||||
if (isHeader)
|
||||
menu.setHeaderTitle(getString(R.string.all_songs));
|
||||
else
|
||||
menu.setHeaderTitle(view.getTitle());
|
||||
@ -515,9 +520,9 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
menu.add(0, MENU_EDIT, 0, R.string.edit).setIntent(intent);
|
||||
}
|
||||
menu.addSubMenu(0, MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist).getItem().setIntent(intent);
|
||||
if (view.hasExpanders())
|
||||
if (adapter != mPlaylistAdapter && adapter != mSongAdapter)
|
||||
menu.add(0, MENU_EXPAND, 0, R.string.expand).setIntent(intent);
|
||||
if (!view.isHeader())
|
||||
if (!isHeader)
|
||||
menu.add(0, MENU_DELETE, 0, R.string.delete).setIntent(intent);
|
||||
}
|
||||
|
||||
@ -538,6 +543,17 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the playlist editor for the playlist with the given id.
|
||||
*/
|
||||
private void editPlaylist(long playlistId, String title)
|
||||
{
|
||||
Intent launch = new Intent(this, PlaylistActivity.class);
|
||||
launch.putExtra("playlist", playlistId);
|
||||
launch.putExtra("title", title);
|
||||
startActivity(launch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the media represented by the given intent and show a Toast
|
||||
* informing the user of this.
|
||||
@ -609,13 +625,9 @@ public class LibraryActivity extends PlaybackActivity implements AdapterView.OnI
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MENU_EDIT: {
|
||||
Intent launch = new Intent(Intent.ACTION_EDIT);
|
||||
launch.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
|
||||
launch.putExtra("playlist", String.valueOf(intent.getLongExtra("id", 0)));
|
||||
startActivity(launch);
|
||||
case MENU_EDIT:
|
||||
editPlaylist(intent.getLongExtra("id", 0), intent.getStringExtra("title"));
|
||||
break;
|
||||
}
|
||||
case MENU_SELECT_PLAYLIST:
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_TO_PLAYLIST, intent));
|
||||
break;
|
||||
|
@ -223,7 +223,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
|
||||
if (pos == 0) {
|
||||
MediaView view;
|
||||
if (convertView == null)
|
||||
view = new MediaView(mContext, mExpandable);
|
||||
view = new MediaView(mContext, null, mExpandable ? MediaView.sExpander : null);
|
||||
else
|
||||
view = (MediaView)convertView;
|
||||
view.makeHeader(mHeaderText);
|
||||
@ -508,7 +508,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer {
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent)
|
||||
{
|
||||
return new MediaView(context, mExpandable);
|
||||
return new MediaView(mContext, null, mExpandable ? MediaView.sExpander : null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,10 +42,16 @@ import android.view.View;
|
||||
* to the right side.
|
||||
*/
|
||||
public final class MediaView extends View {
|
||||
/**
|
||||
* Views that have been made into a header by
|
||||
* {@link MediaView#makeHeader(String)} will be given this id.
|
||||
*/
|
||||
public static final long HEADER_ID = -1;
|
||||
|
||||
/**
|
||||
* The expander arrow bitmap used in all views that have expanders.
|
||||
*/
|
||||
private static Bitmap sExpander;
|
||||
public static Bitmap sExpander;
|
||||
/**
|
||||
* The paint object, cached for reuse.
|
||||
*/
|
||||
@ -76,6 +82,19 @@ public final class MediaView extends View {
|
||||
sPaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached measured view height.
|
||||
*/
|
||||
private final int mViewHeight;
|
||||
/**
|
||||
* An optional bitmap to display on the left of the view.
|
||||
*/
|
||||
private final Bitmap mLeftBitmap;
|
||||
/**
|
||||
* An optional bitmap to display on the right of the view.
|
||||
*/
|
||||
private final Bitmap mRightBitmap;
|
||||
|
||||
/**
|
||||
* The MediaStore id of the media represented by this view.
|
||||
*/
|
||||
@ -89,37 +108,47 @@ public final class MediaView extends View {
|
||||
*/
|
||||
private String mSubTitle;
|
||||
/**
|
||||
* True if the last touch event was over the expander arrow.
|
||||
* True to show the bitmaps, false to hide them. Defaults to true.
|
||||
*/
|
||||
private boolean mExpanderPressed;
|
||||
private boolean mShowBitmaps = true;
|
||||
|
||||
private boolean mBottomGravity;
|
||||
/**
|
||||
* True if the expander should be shown.
|
||||
* The x coordinate of the last touch event.
|
||||
*/
|
||||
private final boolean mExpandable;
|
||||
/**
|
||||
* The cached measured view height.
|
||||
*/
|
||||
private int mViewHeight;
|
||||
/**
|
||||
* True if this view is a header. Will override expandable setting to false.
|
||||
*/
|
||||
private boolean mIsHeader;
|
||||
private int mTouchX;
|
||||
|
||||
/**
|
||||
* Construct a MediaView.
|
||||
*
|
||||
* @param context A Context to use.
|
||||
* @param expandable True if the expander should be shown.
|
||||
* @param leftBitmap An optional bitmap to be shown in the left side of
|
||||
* the view.
|
||||
* @param rightBitmap An optional bitmap to be shown in the right side of
|
||||
* the view.
|
||||
*/
|
||||
public MediaView(Context context, boolean expandable)
|
||||
public MediaView(Context context, Bitmap leftBitmap, Bitmap rightBitmap)
|
||||
{
|
||||
super(context);
|
||||
mLeftBitmap = leftBitmap;
|
||||
mRightBitmap = rightBitmap;
|
||||
|
||||
mExpandable = expandable;
|
||||
int height = 7 * sTextSize / 2;
|
||||
if (mLeftBitmap != null)
|
||||
height = Math.max(height, mLeftBitmap.getHeight() + sTextSize);
|
||||
if (mRightBitmap != null)
|
||||
height = Math.max(height, mRightBitmap.getHeight() + sTextSize);
|
||||
mViewHeight = height;
|
||||
}
|
||||
|
||||
mViewHeight = 7 * sTextSize / 2;
|
||||
if (expandable)
|
||||
mViewHeight = Math.max(mViewHeight, sExpander.getHeight() + sTextSize);
|
||||
/**
|
||||
* Set whether to show the left and right bitmaps. By default, will show them.
|
||||
*
|
||||
* @param show If false, do not show the bitmaps.
|
||||
*/
|
||||
public void setShowBitmaps(boolean show)
|
||||
{
|
||||
mShowBitmaps = show;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,7 +157,10 @@ public final class MediaView extends View {
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mViewHeight);
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY)
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
|
||||
else
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mViewHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,13 +173,17 @@ public final class MediaView extends View {
|
||||
return;
|
||||
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
int height = mViewHeight;
|
||||
int padding = sTextSize / 2;
|
||||
int xOffset = 0;
|
||||
|
||||
if (mBottomGravity)
|
||||
canvas.translate(0, getHeight() - mViewHeight);
|
||||
|
||||
Paint paint = sPaint;
|
||||
|
||||
if (mExpandable && !mIsHeader) {
|
||||
Bitmap expander = sExpander;
|
||||
if (mShowBitmaps && mRightBitmap != null) {
|
||||
Bitmap expander = mRightBitmap;
|
||||
width -= padding * 4 + expander.getWidth();
|
||||
|
||||
paint.setColor(Color.GRAY);
|
||||
@ -158,6 +194,12 @@ public final class MediaView extends View {
|
||||
canvas.drawBitmap(expander, width + padding * 2, (height - expander.getHeight()) / 2, paint);
|
||||
}
|
||||
|
||||
if (mShowBitmaps && mLeftBitmap != null) {
|
||||
Bitmap expander = mLeftBitmap;
|
||||
canvas.drawBitmap(expander, 0, (height - expander.getHeight()) / 2, paint);
|
||||
xOffset = expander.getWidth();
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.clipRect(padding, 0, width - padding, height);
|
||||
|
||||
@ -167,13 +209,13 @@ public final class MediaView extends View {
|
||||
allocatedHeight = height / 2 - padding * 3 / 2;
|
||||
|
||||
paint.setColor(Color.GRAY);
|
||||
canvas.drawText(mSubTitle, padding, height / 2 + padding / 2 + (allocatedHeight - sTextSize) / 2 - paint.ascent(), paint);
|
||||
canvas.drawText(mSubTitle, xOffset + padding, height / 2 + padding / 2 + (allocatedHeight - sTextSize) / 2 - paint.ascent(), paint);
|
||||
} else {
|
||||
allocatedHeight = height - padding * 2;
|
||||
}
|
||||
|
||||
paint.setColor(Color.WHITE);
|
||||
canvas.drawText(mTitle, padding, padding + (allocatedHeight - sTextSize) / 2 - paint.ascent(), paint);
|
||||
canvas.drawText(mTitle, xOffset + padding, padding + (allocatedHeight - sTextSize) / 2 - paint.ascent(), paint);
|
||||
canvas.restore();
|
||||
|
||||
width = getWidth();
|
||||
@ -186,6 +228,27 @@ public final class MediaView extends View {
|
||||
paint.setShader(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the gravity of the view (top or bottom), determing which edge to
|
||||
* align the content to.
|
||||
*
|
||||
* @param bottom True for bottom gravity; false for top gravity.
|
||||
*/
|
||||
public void setBottomGravity(boolean bottom)
|
||||
{
|
||||
mBottomGravity = bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the desired height for this view.
|
||||
*
|
||||
* @return The measured view height.
|
||||
*/
|
||||
public int getPreferredHeight()
|
||||
{
|
||||
return mViewHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MediaStore id of the media represented by this view.
|
||||
*/
|
||||
@ -203,20 +266,11 @@ public final class MediaView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the expander arrow was pressed in the last touch
|
||||
* event.
|
||||
* Returns true if the right bitmap was pressed in the last touch event.
|
||||
*/
|
||||
public boolean isExpanderPressed()
|
||||
public boolean isRightBitmapPressed()
|
||||
{
|
||||
return mExpanderPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if views has expander arrows displayed.
|
||||
*/
|
||||
public boolean hasExpanders()
|
||||
{
|
||||
return mExpandable;
|
||||
return mRightBitmap != null && mShowBitmaps && mTouchX > getWidth() - mRightBitmap.getWidth() - 2 * sTextSize;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,7 +280,8 @@ public final class MediaView extends View {
|
||||
*/
|
||||
public void makeHeader(String text)
|
||||
{
|
||||
mIsHeader = true;
|
||||
mShowBitmaps = false;
|
||||
mId = HEADER_ID;
|
||||
mTitle = text;
|
||||
mSubTitle = null;
|
||||
}
|
||||
@ -242,7 +297,7 @@ public final class MediaView extends View {
|
||||
*/
|
||||
public void updateMedia(Cursor cursor, boolean useSecondary)
|
||||
{
|
||||
mIsHeader = false;
|
||||
mShowBitmaps = true;
|
||||
mId = cursor.getLong(0);
|
||||
mTitle = cursor.getString(1);
|
||||
if (useSecondary)
|
||||
@ -250,21 +305,13 @@ public final class MediaView extends View {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the view is set to be a "Play/Enqueue All" header.
|
||||
*/
|
||||
public boolean isHeader()
|
||||
{
|
||||
return mIsHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mExpanderPressed.
|
||||
*/
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event)
|
||||
{
|
||||
mExpanderPressed = mExpandable && !mIsHeader && event.getX() > getWidth() - sExpander.getWidth() - 2 * sTextSize;
|
||||
mTouchX = (int)event.getX();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -955,7 +955,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
||||
processSong((Song)message.obj);
|
||||
break;
|
||||
case QUERY:
|
||||
runQuery(message.arg1, (QueryTask)message.obj);
|
||||
runQuery(message.arg1, (QueryTask)message.obj, message.arg2);
|
||||
break;
|
||||
case POST_CREATE:
|
||||
mHeadsetPause = getSettings(this).getBoolean("headset_pause", true);
|
||||
@ -1125,15 +1125,22 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
||||
/**
|
||||
* Run the query and add the results to the timeline. Should be called in the
|
||||
* worker thread.
|
||||
*
|
||||
* @param mode How to add songs. Passed to
|
||||
* {@link SongTimeline#addSongs(int, android.database.Cursor, int)}
|
||||
* @param query The query to run.
|
||||
* @param jumpTo Passed to
|
||||
* {@link SongTimeline#addSongs(int, android.database.Cursor, int)}
|
||||
*/
|
||||
public void runQuery(int mode, QueryTask query)
|
||||
public void runQuery(int mode, QueryTask query, int jumpTo)
|
||||
{
|
||||
int count = mTimeline.addSongs(mode, query.runQuery(getContentResolver()));
|
||||
int count = mTimeline.addSongs(mode, query.runQuery(getContentResolver()), jumpTo);
|
||||
|
||||
int text;
|
||||
|
||||
switch (mode) {
|
||||
case SongTimeline.MODE_PLAY:
|
||||
case SongTimeline.MODE_PLAY_JUMP_TO:
|
||||
text = R.plurals.playing;
|
||||
break;
|
||||
case SongTimeline.MODE_PLAY_NEXT:
|
||||
@ -1144,7 +1151,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == SongTimeline.MODE_PLAY && count != 0 && (mState & FLAG_PLAYING) == 0)
|
||||
if ((mode == SongTimeline.MODE_PLAY || mode == SongTimeline.MODE_PLAY_JUMP_TO) && count != 0 && (mState & FLAG_PLAYING) == 0)
|
||||
setFlag(FLAG_PLAYING);
|
||||
|
||||
Toast.makeText(this, getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT).show();
|
||||
@ -1156,10 +1163,12 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
||||
* @param mode One of SongTimeline.MODE_*. Tells whether to play the songs
|
||||
* immediately or enqueue them for later.
|
||||
* @param query The query.
|
||||
* @param jumpTo Passed to
|
||||
* {@link SongTimeline#addSongs(int, android.database.Cursor, int)}
|
||||
*/
|
||||
public void addSongs(int mode, QueryTask query)
|
||||
public void addSongs(int mode, QueryTask query, int jumpTo)
|
||||
{
|
||||
mHandler.sendMessage(mHandler.obtainMessage(QUERY, mode, 0, query));
|
||||
mHandler.sendMessage(mHandler.obtainMessage(QUERY, mode, jumpTo, query));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1194,7 +1203,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
|
||||
}
|
||||
|
||||
String selection = "_id!=" + current.id;
|
||||
addSongs(SongTimeline.MODE_PLAY_NEXT, MediaUtils.buildQuery(type, id, Song.FILLED_PROJECTION, selection));
|
||||
addSongs(SongTimeline.MODE_PLAY_NEXT, MediaUtils.buildQuery(type, id, Song.FILLED_PROJECTION, selection), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,9 +133,10 @@ public class Playlist {
|
||||
ContentValues[] values = new ContentValues[count];
|
||||
for (int i = 0; i != count; ++i) {
|
||||
from.moveToPosition(i);
|
||||
values[i] = new ContentValues(1);
|
||||
values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
|
||||
values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, from.getLong(0));
|
||||
ContentValues value = new ContentValues(2);
|
||||
value.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
|
||||
value.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, from.getLong(0));
|
||||
values[i] = value;
|
||||
}
|
||||
resolver.bulkInsert(uri, values);
|
||||
}
|
||||
|
146
src/org/kreed/vanilla/PlaylistActivity.java
Normal file
146
src/org/kreed/vanilla/PlaylistActivity.java
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.kreed.vanilla;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
|
||||
/**
|
||||
* The playlist activity where playlist songs can be viewed and reordered.
|
||||
*/
|
||||
public class PlaylistActivity extends Activity
|
||||
implements View.OnClickListener
|
||||
, AbsListView.OnItemClickListener
|
||||
{
|
||||
private Looper mLooper;
|
||||
private DragListView mListView;
|
||||
private PlaylistAdapter mAdapter;
|
||||
|
||||
private long mPlaylistId;
|
||||
private boolean mEditing;
|
||||
|
||||
private Button mEditButton;
|
||||
private Button mDeleteButton;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state)
|
||||
{
|
||||
super.onCreate(state);
|
||||
|
||||
HandlerThread thread = new HandlerThread(getClass().getName());
|
||||
thread.start();
|
||||
|
||||
MediaView.init(this);
|
||||
|
||||
setContentView(R.layout.playlist_activity);
|
||||
|
||||
DragListView view = (DragListView)findViewById(R.id.playlist);
|
||||
view.setCacheColorHint(Color.BLACK);
|
||||
view.setDivider(null);
|
||||
view.setFastScrollEnabled(true);
|
||||
view.setOnItemClickListener(this);
|
||||
mListView = view;
|
||||
|
||||
View header = LayoutInflater.from(this).inflate(R.layout.playlist_buttons, null);
|
||||
mEditButton = (Button)header.findViewById(R.id.edit);
|
||||
mEditButton.setOnClickListener(this);
|
||||
mDeleteButton = (Button)header.findViewById(R.id.delete);
|
||||
mDeleteButton.setOnClickListener(this);
|
||||
view.addHeaderView(header);
|
||||
|
||||
mLooper = thread.getLooper();
|
||||
mAdapter = new PlaylistAdapter(this, mLooper);
|
||||
view.setAdapter(mAdapter);
|
||||
|
||||
onNewIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy()
|
||||
{
|
||||
mLooper.quit();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent)
|
||||
{
|
||||
long id = intent.getLongExtra("playlist", 0);
|
||||
mAdapter.setPlaylistId(id);
|
||||
setTitle(intent.getStringExtra("title"));
|
||||
mPlaylistId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable edit mode, which allows songs to be reordered and
|
||||
* removed.
|
||||
*
|
||||
* @param editing True to enable edit mode.
|
||||
*/
|
||||
public void setEditing(boolean editing)
|
||||
{
|
||||
mListView.setEditable(editing);
|
||||
mAdapter.setEditable(editing);
|
||||
int visible = editing ? View.GONE : View.VISIBLE;
|
||||
mDeleteButton.setVisibility(visible);
|
||||
mEditButton.setText(editing ? R.string.done : R.string.edit);
|
||||
mEditing = editing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view)
|
||||
{
|
||||
switch (view.getId()) {
|
||||
case R.id.edit:
|
||||
setEditing(!mEditing);
|
||||
break;
|
||||
case R.id.delete:
|
||||
Playlist.deletePlaylist(getContentResolver(), mPlaylistId);
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id)
|
||||
{
|
||||
if (view instanceof MediaView) {
|
||||
MediaView mediaView = (MediaView)view;
|
||||
if (mediaView.isRightBitmapPressed()) {
|
||||
mAdapter.remove(id);
|
||||
} else if (!mEditing) {
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, Song.FILLED_PLAYLIST_PROJECTION, null);
|
||||
PlaybackService.get(this).addSongs(SongTimeline.MODE_PLAY_JUMP_TO, query, position - mListView.getHeaderViewsCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
237
src/org/kreed/vanilla/PlaylistAdapter.java
Normal file
237
src/org/kreed/vanilla/PlaylistAdapter.java
Normal file
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.kreed.vanilla;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CursorAdapter;
|
||||
|
||||
/**
|
||||
* CursorAdapter backed by MediaStore playlists.
|
||||
*/
|
||||
public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
MediaStore.Audio.Playlists.Members._ID,
|
||||
MediaStore.Audio.Playlists.Members.TITLE,
|
||||
MediaStore.Audio.Playlists.Members.ARTIST,
|
||||
MediaStore.Audio.Playlists.Members.AUDIO_ID,
|
||||
MediaStore.Audio.Playlists.Members.PLAY_ORDER,
|
||||
};
|
||||
|
||||
private final Context mContext;
|
||||
private final Handler mWorkerHandler;
|
||||
private final Handler mUiHandler;
|
||||
|
||||
private long mPlaylistId;
|
||||
|
||||
private boolean mEditable;
|
||||
private final Bitmap mDragBitmap;
|
||||
private final Bitmap mDeleteBitmap;
|
||||
|
||||
/**
|
||||
* Create a playlist adapter.
|
||||
*
|
||||
* @param context A context to use.
|
||||
* @param worker A looper running a worker thread (to run queries on).
|
||||
*/
|
||||
public PlaylistAdapter(Context context, Looper worker)
|
||||
{
|
||||
super(context, null, false);
|
||||
|
||||
mContext = context;
|
||||
mUiHandler = new Handler(this);
|
||||
mWorkerHandler = new Handler(worker, this);
|
||||
|
||||
Resources res = context.getResources();
|
||||
mDragBitmap = BitmapFactory.decodeResource(res, R.drawable.grabber);
|
||||
mDeleteBitmap = BitmapFactory.decodeResource(res, R.drawable.close_normal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the id of the backing playlist.
|
||||
*
|
||||
* @param id The MediaStore id of a playlist.
|
||||
*/
|
||||
public void setPlaylistId(long id)
|
||||
{
|
||||
mPlaylistId = id;
|
||||
mWorkerHandler.sendEmptyMessage(MSG_RUN_QUERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled or disable edit mode. Edit mode adds a drag grabber to the left
|
||||
* side a views and a delete button to the right side of views.
|
||||
*
|
||||
* @param editable True to enable edit mode.
|
||||
*/
|
||||
public void setEditable(boolean editable)
|
||||
{
|
||||
mEditable = editable;
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the values in the given view.
|
||||
*/
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor)
|
||||
{
|
||||
MediaView mediaView = (MediaView)view;
|
||||
mediaView.updateMedia(cursor, true);
|
||||
mediaView.setShowBitmaps(mEditable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new view.
|
||||
*/
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent)
|
||||
{
|
||||
return new MediaView(context, mDragBitmap, mDeleteBitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-run the query. Should be run on worker thread.
|
||||
*/
|
||||
public static final int MSG_RUN_QUERY = 1;
|
||||
/**
|
||||
* Update the cursor. Must be run on UI thread.
|
||||
*/
|
||||
public static final int MSG_UPDATE_CURSOR = 2;
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message message)
|
||||
{
|
||||
switch (message.what) {
|
||||
case MSG_RUN_QUERY: {
|
||||
Cursor cursor = runQuery(mContext.getContentResolver());
|
||||
mUiHandler.sendMessage(mUiHandler.obtainMessage(MSG_UPDATE_CURSOR, cursor));
|
||||
break;
|
||||
}
|
||||
case MSG_UPDATE_CURSOR:
|
||||
changeCursor((Cursor)message.obj);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the playlist songs.
|
||||
*
|
||||
* @param resolver A ContentResolver to query with.
|
||||
* @return The resulting cursor.
|
||||
*/
|
||||
private Cursor runQuery(ContentResolver resolver)
|
||||
{
|
||||
QueryTask query = MediaUtils.buildPlaylistQuery(mPlaylistId, PROJECTION, null);
|
||||
return query.runQuery(resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a song to a new position.
|
||||
*
|
||||
* @param from The old position.
|
||||
* @param to The new position.
|
||||
*/
|
||||
public void move(int from, int to)
|
||||
{
|
||||
if (from == to)
|
||||
// easy mode
|
||||
return;
|
||||
|
||||
int count = getCount();
|
||||
if (to >= count || from >= count)
|
||||
// this can happen when the adapter changes during the drag
|
||||
return;
|
||||
|
||||
// The Android API contains a method to move a playlist item, however,
|
||||
// it has only been available since Froyo and doesn't seem to work
|
||||
// after a song has been removed from the playlist (I think?).
|
||||
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||
Cursor cursor = getCursor();
|
||||
|
||||
int start = Math.min(from, to);
|
||||
int end = Math.max(from, to);
|
||||
|
||||
long order;
|
||||
if (start == 0) {
|
||||
order = 0;
|
||||
} else {
|
||||
cursor.moveToPosition(start - 1);
|
||||
order = cursor.getLong(4) + 1;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(end);
|
||||
long endOrder = cursor.getLong(4);
|
||||
|
||||
// clear the rows we are replacing
|
||||
String[] args = new String[] { Long.toString(order), Long.toString(endOrder) };
|
||||
resolver.delete(uri, "play_order >= ? AND play_order <= ?", args);
|
||||
|
||||
// create the new rows
|
||||
ContentValues[] values = new ContentValues[end - start + 1];
|
||||
for (int i = start, j = 0; i <= end; ++i, ++j, ++order) {
|
||||
cursor.moveToPosition(i == to ? from : i > to ? i - 1 : i + 1);
|
||||
ContentValues value = new ContentValues(2);
|
||||
value.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Long.valueOf(order));
|
||||
value.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, cursor.getLong(3));
|
||||
values[j] = value;
|
||||
}
|
||||
|
||||
// insert the new rows
|
||||
resolver.bulkInsert(uri, values);
|
||||
|
||||
changeCursor(runQuery(resolver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the song with the given id.
|
||||
*
|
||||
* @param id The MediaStore id of the row to remove.
|
||||
*/
|
||||
public void remove(long id)
|
||||
{
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
|
||||
resolver.delete(ContentUris.withAppendedId(uri, id), null, null);
|
||||
changeCursor(runQuery(resolver));
|
||||
}
|
||||
}
|
@ -77,7 +77,7 @@ public class Song implements Comparable<Song> {
|
||||
MediaStore.Audio.Playlists.Members.ALBUM_ID,
|
||||
MediaStore.Audio.Playlists.Members.ARTIST_ID,
|
||||
MediaStore.Audio.Playlists.Members.DURATION,
|
||||
MediaStore.Audio.Media.TRACK,
|
||||
MediaStore.Audio.Playlists.Members.TRACK,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -74,21 +74,28 @@ public final class SongTimeline {
|
||||
/**
|
||||
* Clear the timeline and use only the provided songs.
|
||||
*
|
||||
* @see SongTimeline#addSongs(int,Cursor)
|
||||
* @see SongTimeline#addSongs(int,Cursor,int)
|
||||
*/
|
||||
public static final int MODE_PLAY = 0;
|
||||
/**
|
||||
* Clear the queue and add the songs after the current song.
|
||||
*
|
||||
* @see SongTimeline#addSongs(int,Cursor)
|
||||
* @see SongTimeline#addSongs(int,Cursor,int)
|
||||
*/
|
||||
public static final int MODE_PLAY_NEXT = 1;
|
||||
/**
|
||||
* Add the songs at the end of the timeline, clearing random songs.
|
||||
*
|
||||
* @see SongTimeline#addSongs(int,Cursor)
|
||||
* @see SongTimeline#addSongs(int,Cursor,int)
|
||||
*/
|
||||
public static final int MODE_ENQUEUE = 2;
|
||||
/**
|
||||
* Like play mode, but make the current position point to the song at
|
||||
* the given position.
|
||||
*
|
||||
* @see SongTimeline#addSongs(int,Cursor,int)
|
||||
*/
|
||||
public static final int MODE_PLAY_JUMP_TO = 3;
|
||||
|
||||
/**
|
||||
* Disable shuffle.
|
||||
@ -479,9 +486,10 @@ public final class SongTimeline {
|
||||
*
|
||||
* @param mode How to add the songs. One of SongTimeline.MODE_*.
|
||||
* @param cursor The cursor to fill from.
|
||||
* @param jumpTo The position to jump to for MODE_PLAY_JUMP_TO.
|
||||
* @return The number of songs that were added.
|
||||
*/
|
||||
public int addSongs(int mode, Cursor cursor)
|
||||
public int addSongs(int mode, Cursor cursor, int jumpTo)
|
||||
{
|
||||
if (cursor == null)
|
||||
return 0;
|
||||
@ -506,6 +514,7 @@ public final class SongTimeline {
|
||||
timeline.subList(mCurrentPos + 1, timeline.size()).clear();
|
||||
break;
|
||||
case MODE_PLAY:
|
||||
case MODE_PLAY_JUMP_TO:
|
||||
timeline.clear();
|
||||
mCurrentPos = 0;
|
||||
break;
|
||||
@ -515,16 +524,22 @@ public final class SongTimeline {
|
||||
|
||||
int start = timeline.size();
|
||||
|
||||
Song jumpSong = null;
|
||||
for (int j = 0; j != count; ++j) {
|
||||
cursor.moveToPosition(j);
|
||||
Song song = new Song(-1);
|
||||
song.populate(cursor);
|
||||
timeline.add(song);
|
||||
if (j == jumpTo)
|
||||
jumpSong = song;
|
||||
}
|
||||
|
||||
if (mShuffleMode != SHUFFLE_NONE)
|
||||
MediaUtils.shuffle(timeline.subList(start, timeline.size()), mShuffleMode == SHUFFLE_ALBUMS);
|
||||
|
||||
if (mode == MODE_PLAY_JUMP_TO && jumpSong != null)
|
||||
mCurrentPos = timeline.indexOf(jumpSong);
|
||||
|
||||
broadcastChangedSongs();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user