Version 1.3

This commit is contained in:
Victor 2013-08-24 15:47:48 +03:00
commit 0ac06f6750
39 changed files with 1407 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.classpath
.cproject
.project
assets/
bin/
gen/
obj/
tmp/

44
AndroidManifest.xml Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.annimon.playlisteditor"
android:versionCode="4"
android:versionName="1.3" >
<uses-sdk
android:minSdkVersion="5"
android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".SelectPlaylistActivity"
android:label="@string/app_name"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".EditPlaylistActivity"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:theme="@style/AppTheme.NoTitleBar" />
<activity
android:name="com.google.ads.AdActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" />
</application>
</manifest>

BIN
ic_launcher-web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

11
proguard-project.txt Normal file
View File

@ -0,0 +1,11 @@
@E:\\SETUPS\\Disk\\Programming\\Java\\android.pro
-obfuscationdictionary E:\\SETUPS\\Disk\\Programming\\Java\\compact.txt
-optimizationpasses 9
-allowaccessmodification
-overloadaggressively
#-keep public class android.support.**
# Ad Mob
#-keep class com.google.ads.AdActivity { <init>(...); }
#-keep class com.google.ads.AdView { <init>(...); }
-dontwarn com.google.ads.**

14
project.properties Normal file
View File

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-17

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="@integer/transition_duration"
android:fromXDelta="100%p"
android:toXDelta="0" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="@integer/transition_duration"
android:fromXDelta="0"
android:toXDelta="-100%p" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="@integer/transition_duration"
android:fromXDelta="-100%p"
android:toXDelta="0" />
</set>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="@integer/transition_duration"
android:fromXDelta="0"
android:toXDelta="100%p" />
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:dither="true"
android:src="@drawable/notes_bg_tile_white"
android:tileMode="repeat" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<EditText
android:id="@+id/dialogPlaylistName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_name"
android:inputType="text"
android:maxLength="@integer/maxPlaylistName"
android:singleLine="true" />
<Button
android:id="@+id/dialogOk"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>

27
res/layout/main.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="@android:id/list"
android:layout_alignParentTop="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.ads.AdView
android:id="@+id/adView"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
ads:adSize="BANNER"
ads:adUnitId="a1510ae00ecc03d"
ads:loadAdOnCreate="true"
ads:backgroundColor="#282828"
ads:primaryTextColor="#FFFFFF"
ads:secondaryTextColor="#D7D7D7" />
</RelativeLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="33dp"
android:orientation="horizontal" >
<Spinner
android:id="@+id/sourceSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
<Button
style="?android:attr/buttonStyleSmall"
android:id="@+id/saveButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginRight="5dp"
android:background="@null"
android:drawableLeft="@android:drawable/ic_menu_save"
android:text="@string/save_playlist" />
</RelativeLayout>
<LinearLayout
android:background="@drawable/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<ListView
android:id="@+id/sourcelist"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:choiceMode="singleChoice" />
<ListView
android:id="@+id/resultlist"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

13
res/menu/main.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/menu_new_playlist"
android:showAsAction="always|withText"
android:title="@string/new_playlist"
android:icon="@drawable/ic_new" />
<item android:id="@+id/menu_restore_playlist"
android:showAsAction="never"
android:title="@string/restore_playlist" />
</menu>

18
res/menu/main_context.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/menu_delete_playlist"
android:showAsAction="ifRoom|withText"
android:title="@string/delete"
android:icon="@android:drawable/ic_menu_delete" />
<item android:id="@+id/menu_rename_playlist"
android:showAsAction="ifRoom|withText"
android:title="@string/rename"
android:icon="@android:drawable/ic_menu_edit" />
<item android:id="@+id/menu_backup_playlist"
android:showAsAction="never"
android:title="@string/backup_playlist" />
</menu>

20
res/values-ru/strings.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Playlist Editor</string>
<string name="new_playlist">Создать плейлист</string>
<string name="save_playlist">Сохранить плейлист</string>
<string name="backup_playlist">Резервная копия плейлиста</string>
<string name="restore_playlist">Восстановить плейлист</string>
<string name="delete">Удалить</string>
<string name="rename">Переименовать</string>
<string name="enter_name">Введите имя</string>
<string name="done">Готово!</string>
<string name="all_tracks">Все треки</string>
<!-- Errors, messages etc -->
<string name="error_while_restore_backup">Ошибка при восстановлении резервной копии</string>
<string name="unable_to_store_backup">Невозможно сохранить резервную копию</string>
<string name="empty_backup_list">Пустой список резервных копий</string>
</resources>

12
res/values-v11/styles.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Application and activity styles -->
<style name="AppBaseTheme" parent="android:Theme.Holo.Light" />
<style name="AppTheme.NoTitleBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
</style>
</resources>

5
res/values/integers.xml Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="transition_duration">400</integer>
<integer name="maxPlaylistName">50</integer>
</resources>

20
res/values/strings.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Playlist Editor</string>
<string name="new_playlist">New playlist</string>
<string name="save_playlist">Save playlist</string>
<string name="backup_playlist">Backup playlist</string>
<string name="restore_playlist">Restore playlist</string>
<string name="delete">Delete</string>
<string name="rename">Rename</string>
<string name="enter_name">Enter name</string>
<string name="done">Done!</string>
<string name="all_tracks">All tracks</string>
<!-- Errors, messages etc -->
<string name="error_while_restore_backup">Error while restore backup</string>
<string name="unable_to_store_backup">Unable to store backup</string>
<string name="empty_backup_list">Empty backup list</string>
</resources>

15
res/values/styles.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Application and activity styles -->
<style name="AppBaseTheme" parent="android:Theme.Light" />
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:windowFullscreen">true</item>
</style>
<style name="AppTheme.NoTitleBar">
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View File

@ -0,0 +1,189 @@
package com.annimon.playlisteditor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Spinner;
import com.annimon.playlisteditor.data.Playlist;
import com.annimon.playlisteditor.data.Track;
/**
* Edit playlist window.
* @author aNNiMON
*/
public class EditPlaylistActivity extends Activity {
public static final String PLAYLIST_ID = "playlist_id";
public static final String PLAYLIST_NAME = "playlist_name";
private Track bufferItem;
private TracksAdapter sourceAdapter, resultAdapter;
private ArrayAdapter<Playlist> srcPlaylistAdapter;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.playlist_editor);
initPlaylistSpinner();
((Button) findViewById(R.id.saveButton)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
savePlaylist();
}
});
bufferItem = null;
int playlistId = getIntent().getIntExtra(PLAYLIST_ID, -1);
Track[] tracks = PlaylistDatabase.getTracks(this, playlistId);
int resource = android.R.layout.simple_list_item_1;
resultAdapter = new TracksAdapter(this, resource,
(playlistId == -1) ? new Track[0] : tracks);
ListView listResult = (ListView) findViewById(R.id.resultlist);
listResult.setAdapter(resultAdapter);
listResult.setOnItemClickListener(destItemClick);
listResult.setOnItemLongClickListener(destLongItemClick);
}
@Override
public void onBackPressed() {
super.onBackPressed();
setSlidingTransition();
}
private void initPlaylistSpinner() {
Playlist[] playlists = PlaylistDatabase.getPlaylists(this);
List<Playlist> list = new ArrayList<Playlist>();
list.add(new Playlist(-1, getString(R.string.all_tracks)));
list.addAll(Arrays.asList(playlists));
Spinner sourceSpinner = (Spinner) findViewById(R.id.sourceSpinner);
srcPlaylistAdapter = new ArrayAdapter<Playlist>(this,
android.R.layout.simple_spinner_item, list);
srcPlaylistAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
sourceSpinner.setAdapter(srcPlaylistAdapter);
sourceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> p, View v, int pos, long id) {
int playlistId = srcPlaylistAdapter.getItem(pos).getId();
addTracksToSourceList(playlistId);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
int playlistId = getIntent().getIntExtra(PLAYLIST_ID, -1);
addTracksToSourceList(playlistId);
}
});
}
private void addTracksToSourceList(int playlistId) {
Track[] tracks = PlaylistDatabase.getTracks(this, playlistId);
int resource = android.R.layout.simple_list_item_single_choice;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
resource = android.R.layout.simple_list_item_activated_1;
}
sourceAdapter = new TracksAdapter(this, resource, tracks);
ListView listSource = (ListView) findViewById(R.id.sourcelist);
listSource.setAdapter(sourceAdapter);
listSource.setOnItemClickListener(srcItemClick);
}
/**
* Allow user to enter name of saving playlist.
*/
private void savePlaylist() {
PlaylistNameDialog dialog = new PlaylistNameDialog(this) {
@Override
protected void onClick(String playlistName) {
savePlaylist(playlistName);
// Exit window.
setResult(RESULT_OK);
finish();
setSlidingTransition();
}
};
dialog.setTitle(R.string.save_playlist);
if (getIntent().getIntExtra(PLAYLIST_ID, -1) != -1) {
String playlistName = getIntent().getStringExtra(PLAYLIST_NAME) + "_pe";
dialog.setName(playlistName);
}
dialog.show();
}
/**
* Saves track order into new playlist.
*/
private void savePlaylist(String playlistName) {
int[] trackIds = new int[resultAdapter.getCount()];
for (int i = 0; i < trackIds.length; i++) {
trackIds[i] = resultAdapter.getItem(i).getAudioId();
}
PlaylistDatabase.createPlaylist(this, playlistName, trackIds);
}
/**
* Slide out layout when close screen.
*/
private void setSlidingTransition() {
overridePendingTransition(R.anim.slide_right_in, R.anim.slide_right_out);
}
/**
* Select item for copy into destination list.
* Because it's no way to add item to empty destination list,
* we need automatically add it.
* */
private OnItemClickListener srcItemClick = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
bufferItem = sourceAdapter.getItem(pos);
if (resultAdapter.isEmpty()) {
resultAdapter.addTrack(1, bufferItem);
}
}
};
/** Add item from source list by clicking on destination list. */
private OnItemClickListener destItemClick = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
if (bufferItem != null) {
resultAdapter.addTrack(pos, bufferItem);
}
}
};
/** Remove item on long click in destination list. */
private OnItemLongClickListener destLongItemClick = new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> p, View v, int pos, long id) {
resultAdapter.removeTrack(pos);
return true;
}
};
}

View File

@ -0,0 +1,61 @@
package com.annimon.playlisteditor;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.Log;
/**
* Handling exceptions.
* If error is critical or must be show to user, then
* call <b>alert</b> method.
* In other situations call <b>log</b> method.
*
* @author aNNiMON
*/
public class ExceptionHandler {
private static final boolean DEBUG = false;
private static final String TAG = "ExceptionHandler";
public static void alert(Context context, Exception ex) {
alert(context, getErrorMessage(ex));
}
public static void alert(Context context, int resourceId) {
alert(context, context.getString(resourceId));
}
public static void alert(Context context, String message) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(android.R.string.dialog_alert_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).show();
}
public static void log(Exception ex) {
if (DEBUG) {
Log.e(TAG, getErrorMessage(ex));
}
}
public static void log(String message) {
if (DEBUG) {
Log.e(TAG, message);
}
}
private static String getErrorMessage(Exception ex) {
return ex.getMessage();
}
}

View File

@ -0,0 +1,75 @@
package com.annimon.playlisteditor;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.annimon.playlisteditor.data.BackupFile;
import com.annimon.playlisteditor.data.Playlist;
import android.content.Context;
public class PlaylistBackup {
private static final String BACKUP_EXT = ".bpe";
private Context context;
public PlaylistBackup(Context context) {
this.context = context;
}
public void backup(Playlist playlist) throws IOException {
String playlistName = playlist.getName();
int playlistId = playlist.getId();
String filename = String.valueOf(playlistName.hashCode()) + BACKUP_EXT;
FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
DataOutputStream dos = new DataOutputStream(fos);
PlaylistDatabase.backupPlaylist(context, playlistName, playlistId, dos);
dos.flush();
dos.close();
}
public void restore(BackupFile backup) throws IOException {
FileInputStream fis = context.openFileInput(backup.getFilename());
DataInputStream dis = new DataInputStream(fis);
PlaylistDatabase.restorePlaylist(context, dis);
dis.close();
}
public List<BackupFile> list() throws IOException {
String[] filelist = context.fileList();
int length = filelist.length;
List<BackupFile> backups = new ArrayList<BackupFile>(length);
for (int i = 0; i < length; i++) {
if (filelist[i].endsWith(BACKUP_EXT)) {
String playlistName = getPlaylistName(filelist[i]);
backups.add( new BackupFile(filelist[i], playlistName) );
}
}
return backups;
}
public void delete(String filename) {
File file = context.getFileStreamPath(filename);
if (file != null) {
file.delete();
}
}
private String getPlaylistName(String filename) throws IOException {
FileInputStream fis = context.openFileInput(filename);
DataInputStream dis = new DataInputStream(fis);
String name = dis.readUTF();
dis.close();
return name;
}
}

View File

@ -0,0 +1,194 @@
package com.annimon.playlisteditor;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import com.annimon.playlisteditor.data.Playlist;
import com.annimon.playlisteditor.data.Track;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
public class PlaylistDatabase {
public static void createPlaylist(Context context, String name, int[] trackIds) {
if (TextUtils.isEmpty(name)) return;
ContentValues cv = new ContentValues();
cv.put(MediaStore.Audio.Playlists.NAME, name);
ContentResolver resolver = context.getContentResolver();
Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
int size = trackIds.length;
ContentValues[] values = new ContentValues[size];
for (int i = 0; i < size; i++) {
values[i] = new ContentValues();
values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, i);
values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, trackIds[i]);
}
resolver.bulkInsert(uri, values);
}
public static void deletePlaylist(Context context, int playlistId) {
ContentResolver resolver = context.getContentResolver();
resolver.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Playlists._ID +" = "+ playlistId, null);
}
public static void renamePlaylist(Context context, int id, String name) {
ContentResolver resolver = context.getContentResolver();
ContentValues value = new ContentValues();
value.put(MediaStore.Audio.Playlists.NAME, name);
resolver.update(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
value, MediaStore.Audio.Playlists._ID +" = "+ id, null);
}
public static void backupPlaylist(Context context,
String playlistName, int playlistId,
DataOutputStream dos) throws IOException {
dos.writeUTF(playlistName);
String[] projection = {
MediaStore.Audio.Playlists.Members.AUDIO_ID
};
Cursor tracksCursor = query(context,
MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
projection, null);
int size = tracksCursor.getCount();
dos.writeInt(size);
tracksCursor.moveToFirst();
for (int i = 0; i < size; i++) {
int audioId = tracksCursor.getInt(0);
dos.writeInt(audioId);
tracksCursor.moveToNext();
}
}
public static void restorePlaylist(Context context,
DataInputStream dis) throws IOException {
String playlistName = dis.readUTF();
ContentValues cv = new ContentValues();
cv.put(MediaStore.Audio.Playlists.NAME, playlistName);
ContentResolver resolver = context.getContentResolver();
Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv);
int size = dis.readInt();
ContentValues[] values = new ContentValues[size];
for (int i = 0; i < size; i++) {
values[i] = new ContentValues();
values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, i);
values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, dis.readInt());
}
resolver.bulkInsert(uri, values);
}
public static Playlist[] getPlaylists(Context context) {
String[] projection = {
MediaStore.Audio.Playlists._ID,
MediaStore.Audio.Playlists.NAME
};
Cursor playlistSDCardCursor = query(context,
MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
projection, projection[1]);
int size = playlistSDCardCursor.getCount();
Playlist[] playlists = new Playlist[size];
playlistSDCardCursor.moveToFirst();
for (int i = 0; i < size; i++) {
int id = playlistSDCardCursor.getInt(0);
String name = playlistSDCardCursor.getString(1);
playlists[i] = new Playlist(id, name);
playlistSDCardCursor.moveToNext();
}
return playlists;
}
/**
* Get tracks of playlist. If playlistId equals -1 - return all tracks in system.
* @param context
* @param playlistId id of playlist, or -1.
* @return tracks array.
*/
public static Track[] getTracks(Context context, long playlistId) {
String[] projection = {
(playlistId == -1) ?
MediaStore.Audio.Media._ID :
MediaStore.Audio.Playlists.Members.AUDIO_ID,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.TITLE
};
Cursor tracksCursor;
if (playlistId == -1) {
// Get all tracks.
String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0 ";
tracksCursor = query(context,
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, selection, null, null, 0);
} else {
// Get tracks of playlist.
tracksCursor = query(context,
MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId),
projection, null);
}
int size = tracksCursor.getCount();
Track[] tracks = new Track[size];
tracksCursor.moveToFirst();
for (int i = 0; i < size; i++) {
int audioId = tracksCursor.getInt(0);
String artist = tracksCursor.getString(1);
String title = tracksCursor.getString(2);
tracks[i] = new Track(audioId, artist, title);
tracksCursor.moveToNext();
}
return tracks;
}
private static Cursor query(Context context,
Uri uri, String[] projection, String sortOrder) {
return query(context, uri, projection, null, null, sortOrder, 0);
}
private static Cursor query(Context context, Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder, int limit) {
try {
ContentResolver resolver = context.getContentResolver();
if (resolver == null) {
return null;
}
if (limit > 0) {
uri = uri.buildUpon().appendQueryParameter("limit", String.valueOf(limit)).build();
}
return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
} catch (UnsupportedOperationException ex) {
ExceptionHandler.log(ex);
return null;
}
}
}

View File

@ -0,0 +1,74 @@
package com.annimon.playlisteditor;
import android.app.Dialog;
import android.content.Context;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
/**
* Custom dialog for enter playlist name.
* @author aNNiMON
*/
public abstract class PlaylistNameDialog implements View.OnClickListener {
private Dialog dialog;
private EditText nameEditText;
private Button okButton;
public PlaylistNameDialog(Context context) {
dialog = new Dialog(context);
dialog.setCancelable(true);
dialog.setContentView(R.layout.dialog_name);
nameEditText = (EditText) dialog.findViewById(R.id.dialogPlaylistName);
nameEditText.addTextChangedListener(editTextWatcher);
okButton = (Button) dialog.findViewById(R.id.dialogOk);
okButton.setOnClickListener(this);
okButton.setEnabled(false);
}
public void setTitle(int resource) {
dialog.setTitle(resource);
}
public void setName(String text) {
nameEditText.setText(text);
disableButtonIfTextEmpty();
}
public void show() {
dialog.show();
}
@Override
public void onClick(View v) {
String name = nameEditText.getText().toString();
onClick(name);
dialog.dismiss();
}
protected abstract void onClick(String name);
private void disableButtonIfTextEmpty() {
boolean textEmpty = TextUtils.isEmpty(nameEditText.getText().toString());
okButton.setEnabled(!textEmpty);
}
/** To avoid applying empty text we needs disable button on incorrect situation */
private TextWatcher editTextWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
public void onTextChanged(CharSequence s, int start, int before, int count) { }
public void afterTextChanged(Editable editable) {
disableButtonIfTextEmpty();
}
};
}

View File

@ -0,0 +1,325 @@
package com.annimon.playlisteditor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import com.annimon.playlisteditor.data.BackupFile;
import com.annimon.playlisteditor.data.Playlist;
/**
* Select playlist for edit.
* @author aNNiMON
*/
public class SelectPlaylistActivity extends ListActivity {
private Object actionMode;
private ArrayAdapter<Playlist> adapter;
private int selectedIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
selectedIndex = -1;
ListView listView = getListView();
listView.setBackgroundResource(R.drawable.background);
getPlaylists();
listView.setOnItemClickListener(itemClickListener);
if (isNewApi()) {
listView.setOnItemLongClickListener(itemLongClickListener);
} else {
registerForContextMenu(listView);
}
}
private final Context getContext() {
return SelectPlaylistActivity.this;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_new_playlist:
editPlaylist(-1, "");
break;
case R.id.menu_restore_playlist:
restorePlaylist();
break;
}
return true;
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
if (result == RESULT_OK) {
getPlaylists();
}
}
private void getPlaylists() {
Playlist[] playlists = PlaylistDatabase.getPlaylists(this);
List<Playlist> list = new ArrayList<Playlist>();
list.addAll(Arrays.asList(playlists));
adapter = new ArrayAdapter<Playlist>(this,
android.R.layout.simple_list_item_1, list);
setListAdapter(adapter);
}
private void editPlaylist(int playlistId, String playlistName) {
Intent intent = new Intent();
intent.setClass(getContext(), EditPlaylistActivity.class);
intent.putExtra(EditPlaylistActivity.PLAYLIST_ID, playlistId);
intent.putExtra(EditPlaylistActivity.PLAYLIST_NAME, playlistName);
startActivityForResult(intent, 1);
overridePendingTransition(R.anim.slide_left_in, R.anim.slide_left_out);
}
private void renamePlaylist(final int playlistId) {
PlaylistNameDialog dialog = new PlaylistNameDialog(this) {
@Override
protected void onClick(String newPlaylistName) {
PlaylistDatabase.renamePlaylist(getContext(),
playlistId, newPlaylistName);
// Update list of playlists.
getPlaylists();
}
};
dialog.setTitle(R.string.rename);
String playlistName = adapter.getItem(selectedIndex).getName();
dialog.setName(playlistName);
dialog.show();
}
private void deletePlaylist(int playlistId) {
PlaylistDatabase.deletePlaylist(getContext(), playlistId);
// Update list of playlists.
getPlaylists();
}
private void backupPlaylist(Playlist playlist) {
PlaylistBackup backup = new PlaylistBackup(getContext());
try {
backup.backup(playlist);
Toast.makeText(getContext(), getString(R.string.done),
Toast.LENGTH_LONG).show();
} catch (IOException ex) {
ExceptionHandler.alert(getContext(),
R.string.unable_to_store_backup);
}
}
private void restorePlaylist() {
final PlaylistBackup backup = new PlaylistBackup(getContext());
try {
final List<BackupFile> list = backup.list();
final int size = list.size();
if (size == 0) {
ExceptionHandler.alert(getContext(), R.string.empty_backup_list);
return;
}
String[] playlistNames = new String[size];
final boolean[] checkedItems = new boolean[size];
for (int i = 0; i < size; i++) {
playlistNames[i] = list.get(i).getPlaylistName();
checkedItems[i] = false;
}
new AlertDialog.Builder(getContext())
.setTitle(R.string.restore_playlist)
.setCancelable(true)
// Items
.setMultiChoiceItems(playlistNames, checkedItems,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which,
boolean isChecked) {
checkedItems[which] = isChecked;
}
})
// Restore
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
boolean isRestored = false;
for (int i = 0; i < size; i++) {
try {
if (checkedItems[i]) {
backup.restore(list.get(i));
isRestored = true;
}
} catch (IOException ex) {
ExceptionHandler.alert(getContext(),
getString(R.string.error_while_restore_backup)
+ list.get(i).getPlaylistName());
}
}
if (isRestored) {
Toast.makeText(getContext(), getString(R.string.done),
Toast.LENGTH_LONG).show();
}
// Update list of playlists.
getPlaylists();
}
})
// Delete backups
.setNegativeButton(R.string.delete,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
for (int i = 0; i < size; i++) {
if (checkedItems[i]) {
backup.delete(list.get(i).getFilename());
}
}
}
})
// Cancel
.setNeutralButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.show();
} catch (IOException ex) {
ExceptionHandler.alert(getContext(), ex);
}
}
/** Show editor window by select item. */
private OnItemClickListener itemClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
int playlistId = adapter.getItem(pos).getId();
String playlistName = adapter.getItem(pos).getName();
editPlaylist(playlistId, playlistName);
}
};
/*** API SPECIFIC METHODS ***/
private boolean isNewApi() {
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.main_context, menu);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info =
(AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
selectedIndex = info.position;
actionOrContextMenuItemClicked(item);
return true;
}
private boolean actionOrContextMenuItemClicked(MenuItem item) {
if ( !(0 <= selectedIndex) && (selectedIndex < adapter.getCount()) ) {
return false;
}
int playlistId = adapter.getItem(selectedIndex).getId();
switch (item.getItemId()) {
case R.id.menu_delete_playlist:
deletePlaylist(playlistId);
return true;
case R.id.menu_rename_playlist:
renamePlaylist(playlistId);
return true;
case R.id.menu_backup_playlist:
backupPlaylist(adapter.getItem(selectedIndex));
return true;
}
return false;
}
/** Implements only in SDK >= 11 */
private OnItemLongClickListener itemLongClickListener = new OnItemLongClickListener() {
@SuppressLint("NewApi")
@Override
public boolean onItemLongClick(AdapterView<?> p, View v, int pos, long id) {
if (actionMode != null) return false;
selectedIndex = pos;
if (isNewApi()) {
actionMode = startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.main_context, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (actionOrContextMenuItemClicked(item)) {
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
}
});
}
v.setSelected(true);
return true;
}
};
}

View File

@ -0,0 +1,85 @@
package com.annimon.playlisteditor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.annimon.playlisteditor.data.Track;
/**
* Adapter for track info.
* @author aNNiMON
*/
public class TracksAdapter extends ArrayAdapter<Track> {
private int resource;
private LayoutInflater inflater;
private List<Track> objects;
public TracksAdapter(Context context, int resource, Track[] tracksArray) {
super(context, resource, tracksArray);
this.resource = resource;
objects = new ArrayList<Track>();
objects.addAll(Arrays.asList(tracksArray));
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void addTrack(int position, Track track) {
if (position > objects.size()) {
objects.add(track);
} else {
objects.add(position, track);
}
notifyDataSetChanged();
}
public void removeTrack(int position) {
objects.remove(position);
notifyDataSetChanged();
}
@Override
public int getCount() {
return objects.size();
}
@Override
public Track getItem(int position) {
return objects.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
v = inflater.inflate(resource, null);
holder.text = (TextView) v.findViewById(android.R.id.text1);
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
holder.text.setText(objects.get(position).toString());
return v;
}
private static class ViewHolder {
TextView text;
}
}

View File

@ -0,0 +1,21 @@
package com.annimon.playlisteditor.data;
public class BackupFile {
private String filename;
private String playlistName;
public BackupFile(String filename, String playlistName) {
this.filename = filename;
this.playlistName = playlistName;
}
public String getFilename() {
return filename;
}
public String getPlaylistName() {
return playlistName;
}
}

View File

@ -0,0 +1,36 @@
package com.annimon.playlisteditor.data;
public class Playlist {
private int id;
private String name;
public Playlist(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,23 @@
package com.annimon.playlisteditor.data;
public class Track {
private int audioId;
private String artist, title;
public Track(int audioId, String artist, String title) {
this.audioId = audioId;
this.artist = artist;
this.title = title;
}
public int getAudioId() {
return audioId;
}
@Override
public String toString() {
return artist + " - " + title;
}
}