Add new PlaylistObserver which deprecates PlaylistBridge

This commit is contained in:
Adrian Ulrich 2018-07-07 11:13:16 +02:00
parent 92c4273a5e
commit b957b49e72
7 changed files with 392 additions and 178 deletions

View File

@ -1,95 +0,0 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.medialibrary;
import android.content.Context;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore.Audio;
import android.util.Log;
import java.util.ArrayList;
class PlaylistBridge {
/**
* Queries all native playlists and imports them
*
* @param context the context to use
*/
static void importAndroidPlaylists(Context context) {
ContentResolver resolver = context.getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Audio.Playlists.EXTERNAL_CONTENT_URI, new String[]{Audio.Playlists._ID, Audio.Playlists.NAME}, null, null, null);
} catch (SecurityException e) {
Log.v("VanillaMusic", "Unable to query existing playlists, exception: "+e);
}
if (cursor != null) {
while (cursor.moveToNext()) {
long playlistId = cursor.getLong(0);
String playlistName = cursor.getString(1);
importAndroidPlaylist(context, playlistName, playlistId);
}
cursor.close();
}
}
/**
* Imports a single native playlist into our own media library
*
* @param context the context to use
* @param targetName the name of the playlist in our media store
* @param playlistId the native playlist id to import
*/
static void importAndroidPlaylist(Context context, String targetName, long playlistId) {
ArrayList<Long> bulkIds = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
Uri uri = Audio.Playlists.Members.getContentUri("external", playlistId);
Cursor cursor = null;
try {
cursor = resolver.query(uri, new String[]{Audio.Media.DATA}, null, null, Audio.Playlists.Members.DEFAULT_SORT_ORDER);
} catch (SecurityException e) {
Log.v("VanillaMusic", "Failed to query playlist: "+e);
}
if (cursor != null) {
while (cursor.moveToNext()) {
String path = cursor.getString(0);
// We do not need to do a lookup by path as we can calculate the id used
// by the mediastore using the path
bulkIds.add(MediaLibrary.hash63(path));
}
cursor.close();
}
if (bulkIds.size() == 0)
return; // do not import empty playlists
long targetPlaylistId = MediaLibrary.createPlaylist(context, targetName);
if (targetPlaylistId == -1)
return; // already exists, won't touch
MediaLibrary.addToPlaylist(context, targetPlaylistId, bulkIds);
}
}

View File

@ -506,7 +506,8 @@ public final class PlaybackService extends Service
mRemoteControlClient = new RemoteControl().getClient(this);
mRemoteControlClient.initializeRemote();
mPlaylistObserver = new PlaylistObserver(this);
int syncMode = Integer.parseInt(settings.getString(PrefKeys.PLAYLIST_SYNC_MODE, PrefDefaults.PLAYLIST_SYNC_MODE));
mPlaylistObserver = new PlaylistObserver(this, syncMode);
mLooper = thread.getLooper();
mHandler = new Handler(mLooper, this);
@ -940,6 +941,9 @@ public final class PlaybackService extends Service
mReadaheadEnabled = settings.getBoolean(PrefKeys.ENABLE_READAHEAD, PrefDefaults.ENABLE_READAHEAD);
} else if (PrefKeys.AUTOPLAYLIST_PLAYCOUNTS.equals(key)) {
mAutoPlPlaycounts = settings.getInt(PrefKeys.AUTOPLAYLIST_PLAYCOUNTS, PrefDefaults.AUTOPLAYLIST_PLAYCOUNTS);
} else if (PrefKeys.PLAYLIST_SYNC_MODE.equals(key)) {
int syncMode = Integer.parseInt(settings.getString(PrefKeys.PLAYLIST_SYNC_MODE, PrefDefaults.PLAYLIST_SYNC_MODE));
mPlaylistObserver.setSyncMode(syncMode);
} else if (PrefKeys.SELECTED_THEME.equals(key) || PrefKeys.DISPLAY_MODE.equals(key)) {
// Theme changed: trigger a restart of all registered activites
ArrayList<TimelineCallback> list = sCallbacks;

View File

@ -26,23 +26,36 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import android.os.FileObserver;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.zip.CRC32;
import android.util.Log;
public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callback {
/**
* Timeout to coalesce duplicate messages.
* Bits for mSyncMode
*/
private final static int COALESCE_EVENTS_DELAY_MS = 280;
public final static int SYNC_MODE_IMPORT = (1 << 0);
public final static int SYNC_MODE_EXPORT = (1 << 1);
public final static int SYNC_MODE_PURGE = (1 << 2);
/**
* Timeout to coalesce duplicate messages, ~2.3 sec because no real reason.
*/
private final static int COALESCE_EVENTS_DELAY_MS = 2345;
/**
* Extension to use for M3U files
*/
@ -63,30 +76,42 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
* Directory which holds observed playlists.
*/
private File mPlaylists = new File(Environment.getExternalStorageDirectory(), "Playlists");
static class Database {
/**
* What kind of synching to perform, bitmask of PlaylistObserver.SYNC_MODE_*
*/
private int mSyncMode;
/**
* Database fields
*/
private static class Database {
final static String TABLE_NAME = "playlist_metadata";
final static String _ID = "_id";
final static String NAME = "name";
final static String MTIME = "mtime";
final static String HASH = "hash";
final static String[] FILLED_PROJECTION = {
_ID,
NAME,
MTIME,
HASH,
};
}
public PlaylistObserver(Context context) {
super(context, "playlistobserver.db", null, 1 /* version */);
public PlaylistObserver(Context context, int mode) {
super(context, "playlist_observer.db", null, 1 /* version */);
mContext = context;
setSyncMode(mode);
// Launch new thread for background execution
mHandlerThread= new HandlerThread("PlaylistWriter", Process.THREAD_PRIORITY_LOWEST);
mHandlerThread= new HandlerThread("PlaylisObserverHandler", Process.THREAD_PRIORITY_LOWEST);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper(), this);
// Register to receive media library events.
MediaLibrary.registerLibraryObserver(mObserver);
MediaLibrary.registerLibraryObserver(mLibraryObserver);
mFileObserver.startWatching();
XT("Object created, trigger FULL_SYNC_SCAN");
sendUniqueMessage(MSG_FULL_SYNC_SCAN, 0);
}
/**
@ -94,28 +119,81 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
* after this function was called.
*/
public void unregister() {
MediaLibrary.unregisterLibraryObserver(mObserver);
MediaLibrary.unregisterLibraryObserver(mLibraryObserver);
mFileObserver.stopWatching();
mHandlerThread.quitSafely();
mHandlerThread = null;
mHandler = null;
}
/**
* Change the sync mode of a created instance
*
* @param mode the new mode
*/
public void setSyncMode(int mode) {
mSyncMode = mode;
XT("Sync mode is now "+mSyncMode);
}
/**
* SQLiteHelper onCreate
*/
@Override
public void onCreate(SQLiteDatabase dbh) {
dbh.execSQL("CREATE TABLE "+Database.TABLE_NAME+" ( "
+ Database._ID + " INTEGER PRIMARY KEY, "
+ Database.HASH + " INTEGER NOT NULL, "
+ Database.NAME + " TEXT NOT NULL )"
);
}
/**
* SQLiteHelper onUpgrade
*/
@Override
public void onUpgrade(SQLiteDatabase dbh, int oldVersion, int newVersion) {
// No updates so far.
}
/**
* Message handler, used to dedupe messages and perform
* background work.
*/
private final static int MSG_DUMP_M3U = 1;
private final static int MSG_DUMP_ALL_M3U = 2;
private final static int MSG_IMPORT_M3U = 3;
private final static int MSG_FORCE_M3U_IMPORT = 4;
private final static int MSG_FULL_SYNC_SCAN = 5;
ArrayList<Integer> msgDedupe = new ArrayList<>();
@Override
public boolean handleMessage(Message message) {
msgDedupe.remove(0);
switch (message.what) {
case MSG_DUMP_M3U:
Long id = (Long)message.obj;
if (!dumpM3uPlaylist(id)) {
// Dump of 'id' failed, so this playlist was likely deleted.
cleanupOrphanedM3u();
if (Playlist.getPlaylist(mContext, id) != null) {
XT("DUMP_M3U: source of id "+id+" exists, dumping");
dumpAsM3uPlaylist(id);
} else {
XT("DUMP_M3U: source of id "+id+" vanished, scanning all");
sendUniqueMessage(MSG_FULL_SYNC_SCAN, 0);
}
break;
case MSG_DUMP_ALL_M3U:
dumpM3uPlaylists();
cleanupOrphanedM3u();
dumpAllAsM3uPlaylist();
break;
case MSG_IMPORT_M3U:
File f = (File)(message.obj);
importM3uPlaylist(f);
break;
case MSG_FORCE_M3U_IMPORT:
forceM3uImport();
break;
case MSG_FULL_SYNC_SCAN:
fullSyncScan();
break;
default:
throw new IllegalArgumentException("Invalid message type received");
@ -123,32 +201,141 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
return true;
}
/**
* Forcefully re-imports all M3U files, even if we think that
* our information is up-to-date.
*/
private void forceM3uImport() {
Cursor cursor = queryDatabase(null);
if (cursor != null) {
while (cursor.moveToNext()) {
deletePlaylistMetadata(cursor.getLong(0));
}
cursor.close();
}
// run this ASAP to ensure that no other message re-populates
// metadata.
XT("forceM3uImport: metadata cleared, calling fullSyncScan");
fullSyncScan();
}
/**
* Dumps all playlist to stable storage.
*/
private void dumpAllAsM3uPlaylist() {
XT("dumpAllAsM3uPlaylist: called");
Cursor cursor = Playlist.queryPlaylists(mContext);
if (cursor != null) {
while(cursor.moveToNext()) {
final long id = cursor.getLong(0);
XT("dumpAllAsM3uPlaylist: Dumping ID "+id);
sendUniqueMessage(MSG_DUMP_M3U, id);
}
cursor.close();
}
}
/**
* Adds a new message to the queue. Ignores call if a duplicate
* message is already pending.
*
* @param type the type of the message
* @param obj object payload of this message.
*/
private void sendUniqueMessage(int type, Object obj) {
int fprint = type << 10 + obj.hashCode();
if (!msgDedupe.contains(fprint)) {
msgDedupe.add(fprint);
mHandler.sendMessageDelayed(mHandler.obtainMessage(type, obj), COALESCE_EVENTS_DELAY_MS);
}
}
/**
* Imports an M3U formatted file into our native media library.
*
* @param m3u the file to import
*/
private void importM3uPlaylist(File m3u) {;
XT("importM3uPlaylist("+m3u+")");
if ((mSyncMode & SYNC_MODE_IMPORT) == 0)
return;
if (!m3u.exists())
return;
final long hash = getHash(m3u);
if (hash == -1)
return;
boolean must_import = true;
String import_as = fromM3u(m3u.getName());
Cursor cursor = queryDatabase(null);
if (cursor != null) {
// Try to find an existing playlist where the constructed path
// would match given input file.
while(cursor.moveToNext()) {
File tmp = getFileForName(mPlaylists, asM3u(cursor.getString(1)));
if (m3u.equals(tmp)) {
// Found a matching playlist: this will be our import target
// if the hash indicates that our version is outdated.
import_as = cursor.getString(1);
must_import = (hash != cursor.getLong(2));
XT("importM3uPlaylist(): hash="+hash+", import="+must_import+", import_as="+import_as);
break;
}
}
cursor.close();
}
if (must_import) {
MediaLibrary.unregisterLibraryObserver(mLibraryObserver);
long import_id = Playlist.createPlaylist(mContext, import_as);
try (BufferedReader br = new BufferedReader(new FileReader(m3u))) {
String line;
while ((line = br.readLine()) != null) {
if (line.matches("^/.+")) {
Playlist.addToPlaylist(mContext, import_id, MediaUtils.buildFileQuery(line, Song.FILLED_PROJECTION));
}
}
updatePlaylistMetadata(import_id, import_as, hash);
} catch(IOException e) {
Log.e("VanillaMusic", "Error while parsing m3u: "+e);
}
MediaLibrary.registerLibraryObserver(mLibraryObserver);
}
}
/**
* Exports a single playlist ad M3U(8).
*
* @param id the playlist id to export.
* @return true if the playlist was dumped.
* @return the newly written playlist, null if nothing was done.
*/
private boolean dumpM3uPlaylist(long id) {
final String name = Playlist.getPlaylist(mContext, id);
private File dumpAsM3uPlaylist(long id) {
XT("dumpM3uPlaylist("+id+")");
if (id < 0)
throw new IllegalArgumentException("Called with negative id!");
if ((mSyncMode & SYNC_MODE_EXPORT) == 0)
return null;
final String name = Playlist.getPlaylist(mContext, id);
if (name == null)
return false;
return null;
final File m3u = getFileForName(mPlaylists, asM3u(name));
if (!mPlaylists.isDirectory())
mPlaylists.mkdir();
Log.v("VanillaMusic", "Dumping "+getFileForName(mPlaylists, name));
PrintWriter pw = null;
QueryTask query = MediaUtils.buildPlaylistQuery(id, Song.FILLED_PLAYLIST_PROJECTION);
Cursor cursor = query.runQuery(mContext);
try {
if (cursor != null) {
pw = new PrintWriter(getFileForName(mPlaylists, name + M3U_EXT));
pw = new PrintWriter(m3u);
pw.println("#EXTM3U");
while (cursor.moveToNext()) {
final String path = cursor.getString(1);
@ -159,7 +346,9 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
pw.printf("#EXTINF:%d,%s - %s%n", (duration/1000), artist, title);
pw.println(path);
}
updatePlaylistMetadata(id, name);
pw.flush();
long hash_new = getHash(m3u);
updatePlaylistMetadata(id, name, hash_new);
}
} catch (IOException e) {
Log.v("VanillaMusic", "IOException while writing:", e);
@ -167,50 +356,64 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
if (cursor != null) cursor.close();
if (pw != null) pw.close();
}
return true;
return m3u;
}
/**
* Dumps all playlist to stable storage.
* Identify (and remove) playlist items which do not exist anymore
* and pick up any new M3U files.
*/
private void dumpM3uPlaylists() {
Cursor cursor = Playlist.queryPlaylists(mContext);
if (cursor != null) {
while(cursor.moveToNext()) {
final long id = cursor.getLong(0);
sendUniqueMessage(MSG_DUMP_M3U, id);
}
cursor.close();
}
}
private void fullSyncScan() {
XT("fullSyncScan() running...");
ArrayList<File> knownM3u = new ArrayList<>();
/**
* Checks our playlists directory for files which reference
* non-existing playlists and removes them.
*/
private void cleanupOrphanedM3u() {
SQLiteDatabase dbh = getReadableDatabase();
Cursor cursor = dbh.query(Database.TABLE_NAME, Database.FILLED_PROJECTION, null, null, null, null, null);
// First step is to check all known playlist metadata entries
// and check whether their native or M3U copy was purged.
final boolean do_purge = (mSyncMode & SYNC_MODE_PURGE) != 0;
Cursor cursor = queryDatabase(null);
if (cursor != null) {
while (cursor.moveToNext()) {
final long id = cursor.getLong(0);
final String name = cursor.getString(1);
final File src_m3u = getFileForName(mPlaylists, name + M3U_EXT);
// generates possible names of this playlist as M3U.
final File src_m3u = getFileForName(mPlaylists, asM3u(name));
final File bak_m3u = getFileForName(mPlaylists, name + ".backup");
if (Playlist.getPlaylist(mContext, id) == null) {
// Native version of this playlist is gone, rename M3U variant:
File dst_m3u = getFileForName(mPlaylists, name + ".bak");
src_m3u.renameTo(dst_m3u);
if (do_purge) {
src_m3u.renameTo(bak_m3u);
}
deletePlaylistMetadata(id);
Log.v("VanillaMusic", name+": Renamed old m3u");
} else if (!src_m3u.exists()) {
Playlist.deletePlaylist(mContext, id); // Fixme: do we really want this?
XT("fullSyncScan(): renamed old M3U -> "+bak_m3u);
} else if (do_purge && !src_m3u.exists()) {
// Source vanished, write one last dump and remove it.
File dump = dumpAsM3uPlaylist(id);
if (dump != null) {
dump.renameTo(bak_m3u);
}
Playlist.deletePlaylist(mContext, id);
deletePlaylistMetadata(id);
Log.v("VanillaMusic", name+": Killed native playlist");
XT("fullSyncScan(): killed native playlist with id "+id);
}
// If this M3U exists, record it so that we don't try to re-import.
if (src_m3u.exists()) {
knownM3u.add(src_m3u);
}
}
cursor.close();
}
// Now list all M3U files in the playlists dir and import newly seen files.
File[] files = mPlaylists.listFiles();
if (files != null) {
for (File f : files) {
if (isM3uFilename(f.getName()) && !knownM3u.contains(f)) {
XT("fullSyncScan(): new M3U discovered, must import "+f);
sendUniqueMessage(MSG_IMPORT_M3U, f);
}
}
}
}
/**
@ -220,7 +423,6 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
* @return file object for given name
*/
private File getFileForName(File parent, String name) {
//Fixme: check for m3u8 and remove invalid chars.
name = name.replaceAll("/", "_");
File f = new File(parent, name);
return f;
@ -232,12 +434,16 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
* @param id the id to update.
* @param name the name to register for this id.
*/
private void updatePlaylistMetadata(long id, String name) {
private void updatePlaylistMetadata(long id, String name, long hash) {
if (hash < 0)
throw new IllegalArgumentException("hash can not be negative");
XT("updatePlaylistMetadata of "+name+" to hash "+hash);
SQLiteDatabase dbh = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(Database._ID, id);
values.put(Database.NAME, name);
values.put(Database.MTIME, System.currentTimeMillis());
values.put(Database.HASH, hash);
deletePlaylistMetadata(id);
dbh.insert(Database.TABLE_NAME, null, values);
@ -250,7 +456,7 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
*/
private void deletePlaylistMetadata(long id) {
SQLiteDatabase dbh = getWritableDatabase();
dbh.delete(Database.TABLE_NAME, Database._ID+"=?", new String[] { new Long(id).toString() });
dbh.delete(Database.TABLE_NAME, Database._ID+"=?", new String[] { Long.valueOf(id).toString() });
}
/**
@ -259,7 +465,7 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
* @param name the name to check
* @return true if file appears to be an M3U
*/
private boolean FIXME_isM3uFilename(String name) {
private boolean isM3uFilename(String name) {
if (name.length() < M3U_EXT.length())
return false;
final int offset = name.length() - M3U_EXT.length();
@ -267,48 +473,120 @@ public class PlaylistObserver extends SQLiteOpenHelper implements Handler.Callba
}
/**
* Adds a new message to the queue. Pending duplicate messages
* will be pruged.
* Returns the m3u-filename of name
*
* @param type the type of the message
* @param obj object payload of this message.
* @param name the name to use
* @return the m3u name
*/
private void sendUniqueMessage(int type, Long obj) {
mHandler.removeMessages(type, obj);
mHandler.sendMessageDelayed(mHandler.obtainMessage(type, obj), COALESCE_EVENTS_DELAY_MS);
private String asM3u(String name) {
return name + M3U_EXT;
}
/**
* Returns the name of an m3u file
*
* @param name the m3u filename
* @return the non-m3u name
*/
private String fromM3u(String name) {
if (!isM3uFilename(name))
throw new IllegalArgumentException("Not an M3U filename: "+name);
return name.substring(0, name.length() - M3U_EXT.length());
}
/**
* Hashes the contents of given file
*
* @param f the file to hash
* @return the calculated hash, -1 on error.
*/
private long getHash(File f) {
long hash = -1;
byte[] buff = new byte[4096];
try(FileInputStream fis = new FileInputStream(f)) {
CRC32 crc = new CRC32();
while(fis.read(buff) != -1) {
crc.update(buff);
}
hash = crc.getValue();
if (hash < 0)
hash = hash * -1;
} catch(IOException e) {
// hash will be -1 which signals failure.
}
return hash;
}
/**
* Obtain a cursor to our metadaata database.
*
* @param selection selection for query
* @return cursor with results.
*/
private Cursor queryDatabase(String selection) {
return getReadableDatabase().query(Database.TABLE_NAME, Database.FILLED_PROJECTION, selection, null, null, null, null);
}
/**
* Library observer callback which notifies us about media library
* events.
*
* @param type the event type
* @param id the id of given type which had a change
* @param ongoing whether or not to expect more of these events
*/
private final LibraryObserver mObserver = new LibraryObserver() {
private final LibraryObserver mLibraryObserver = new LibraryObserver() {
@Override
public void onChange(LibraryObserver.Type type, long id, boolean ongoing) {
if (type != LibraryObserver.Type.PLAYLIST || ongoing)
return;
// Dispatch this event but use different type if id was -1 as
// this indicates that multiple (unknown) playlists may have changed.
final int msg = (id < 0 ? MSG_DUMP_ALL_M3U : MSG_DUMP_M3U);
int msg = MSG_DUMP_M3U; // Default: export this playlist ID.
if (id == LibraryObserver.Value.UNKNOWN) {
// An unknown (all?) playlist was modified: dump all to M3U
msg = MSG_DUMP_ALL_M3U;
}
if (id == LibraryObserver.Value.OUTDATED) {
// Our data is wrong, reimport all M3Us
msg = MSG_FORCE_M3U_IMPORT;
}
XT("LibraryObserver::onChange id="+id+", msg="+msg);
sendUniqueMessage(msg, id);
}
};
@Override
public void onCreate(SQLiteDatabase dbh) {
dbh.execSQL("CREATE TABLE "+Database.TABLE_NAME+" ( "
+ Database._ID + " INTEGER PRIMARY KEY, "
+ Database.MTIME + " INTEGER NOT NULL, "
+ Database.NAME + " TEXT NOT NULL )"
);
}
/**
* Observer which monitors the playlists directory.
*
* @param event the event type
* @param dirent the filename which triggered the event.
*/
private final static int mask = FileObserver.CLOSE_WRITE | FileObserver.MOVED_FROM | FileObserver.MOVED_TO | FileObserver.DELETE;
private final FileObserver mFileObserver = new FileObserver(mPlaylists.getAbsolutePath(), mask) {
@Override
public void onEvent(int event, String dirent) {
if (!isM3uFilename(dirent))
return;
@Override
public void onUpgrade(SQLiteDatabase dbh, int oldVersion, int newVersion) {
// No updates so far.
if ((event & (FileObserver.MOVED_FROM | FileObserver.DELETE)) != 0) {
// A M3U vanished, do a full scan.
XT("FileObserver::onEvent DELETE of "+dirent+" triggers FULL_SYNC_SCAN");
sendUniqueMessage(MSG_FULL_SYNC_SCAN, 0);
}
if ((event & (FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE)) != 0) {
// Single file was created, import it.
XT("FileObserver::onEvent WRITE of "+dirent+" triggers IMPORT_M3U");
sendUniqueMessage(MSG_IMPORT_M3U, new File(mPlaylists, dirent));
}
}
};
private void XT(String s) {
try(PrintWriter pw = new PrintWriter(new FileOutputStream(new File("/sdcard/playlist-observer.txt"), true))) {
pw.println(System.currentTimeMillis()/1000+": "+s);
Log.v("VanillaMusic", "XTRACE: "+s);
} catch(Exception e) {
}
}
// TODO:
// Use FileObserver to track playlist changes?
// how do we check modifications? write a shadow-dir in private app storage with same mtimes?
}

View File

@ -70,5 +70,5 @@ public class PrefDefaults {
public static final boolean IGNORE_AUDIOFOCUS_LOSS = false;
public static final boolean ENABLE_SCROLL_TO_SONG = false;
public static final boolean KEEP_SCREEN_ON = false;
public static final int PLAYLIST_SYNC_MODE = 0xFF;
public static final String PLAYLIST_SYNC_MODE = "255";
}

View File

@ -330,6 +330,12 @@ THE SOFTWARE.
<string name="autoplaylist_playcounts_disabled">Do not create an automatic playlist</string>
<string name="autoplaylist_playcounts_name" formatted="false">Top %d</string>
<string name="playlist_sync_mode_title">Playlist synchronization</string>
<string name="playlist_sync_all">Synchronize \'Playlists\' folder</string>
<string name="playlist_sync_only_import">Import M3U, never export</string>
<string name="playlist_sync_only_export">Write M3U, never import</string>
<string name="playlist_sync_disabled">Disabled</string>
<string name="permission_request_summary">Vanilla Music needs read permission to display your music library</string>
<string name="reverse_sort">Reverse sort</string>

View File

@ -127,6 +127,21 @@ THE SOFTWARE.
</string-array>
<!-- END default action entries definition -->
<!-- START playlist sync mode entries definition -->
<string-array name="playlist_sync_mode_entries">
<item>@string/playlist_sync_all</item>
<item>@string/playlist_sync_only_export</item>
<item>@string/playlist_sync_only_import</item>
<item>@string/playlist_sync_disabled</item>
</string-array>
<string-array name="playlist_sync_mode_values">
<item>255</item> <!-- default, sync all -->
<item>2</item> <!-- SYNC_MODE_EXPORT -->
<item>1</item> <!-- SYNC_MODE_IMPORT -->
<item>0</item> <!-- DISABLE -->
</string-array>
<!-- END playlist sync mode entries definition -->
<string-array name="notification_visibility_entries">
<item>@string/show_when_playing</item>
<item>@string/always_show</item>

View File

@ -60,6 +60,12 @@ THE SOFTWARE.
vanilla:sbpSummaryText="@string/autoplaylist_playcounts_summary"
vanilla:sbpSummaryFormat="@string/autoplaylist_playcounts_fmt"
vanilla:sbpSummaryZeroText="@string/autoplaylist_playcounts_disabled"/>
<ch.blinkenlights.android.vanilla.ListPreferenceSummary
android:key="playlist_sync_mode"
android:title="@string/playlist_sync_mode_title"
android:entries="@array/playlist_sync_mode_entries"
android:entryValues="@array/playlist_sync_mode_values"
android:defaultValue="255" />
<CheckBoxPreference
android:key="scrobble"
android:title="@string/scrobble_title"