diff --git a/res/values/translatable.xml b/res/values/translatable.xml
index 47a00a3c..03c98464 100644
--- a/res/values/translatable.xml
+++ b/res/values/translatable.xml
@@ -313,4 +313,7 @@ THE SOFTWARE.
Number of tracks
Play time (Hours)
Scan progress
+
+ Scanning your media library, this might take some time
+ Media library scan finished!
diff --git a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java
index 260942c3..8a4b7054 100644
--- a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java
+++ b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java
@@ -17,6 +17,8 @@
package ch.blinkenlights.android.medialibrary;
+import ch.blinkenlights.android.vanilla.R;
+
import android.content.Context;
import android.content.ContentValues;
import android.content.SharedPreferences;
@@ -29,6 +31,8 @@ import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
+import android.widget.Toast;
+
import java.io.File;
import java.util.ArrayList;
import java.util.regex.Pattern;
@@ -50,6 +54,11 @@ public class MediaScanner implements Handler.Callback {
* Instance of a media backend
*/
private MediaLibraryBackend mBackend;
+ /**
+ * True if this is a from-scratch import
+ * Set by KICKSTART rpc
+ */
+ private boolean mIsInitialScan;
MediaScanner(Context context, MediaLibraryBackend backend) {
@@ -77,7 +86,7 @@ public class MediaScanner implements Handler.Callback {
public void startNormalScan() {
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null)
.addNextStep(RPC_LIBRARY_VRFY, null);
- mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NOOP, 0));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_KICKSTART, 0));
}
/**
@@ -89,7 +98,7 @@ public class MediaScanner implements Handler.Callback {
}
mScanPlan.addNextStep(RPC_LIBRARY_VRFY, null);
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null);
- mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NOOP, 0));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_KICKSTART, 0));
}
/**
@@ -102,7 +111,7 @@ public class MediaScanner implements Handler.Callback {
if (!mHandler.hasMessages(MSG_SCAN_RPC)) {
mScanPlan.addNextStep(RPC_NATIVE_VRFY, null)
.addOptionalStep(RPC_LIBRARY_VRFY, null); // only runs if previous scan found no change
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_NOOP, 0), delay);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCAN_RPC, RPC_KICKSTART, 0), delay);
}
}
@@ -125,8 +134,9 @@ public class MediaScanner implements Handler.Callback {
}
private static final int MSG_SCAN_RPC = 0;
- private static final int MSG_NOTIFY_CHANGE = 1;
- private static final int RPC_NOOP = 100;
+ private static final int MSG_SCAN_FINISHED = 1;
+ private static final int MSG_NOTIFY_CHANGE = 2;
+ private static final int RPC_KICKSTART = 100;
private static final int RPC_READ_DIR = 101;
private static final int RPC_INSPECT_FILE = 102;
private static final int RPC_LIBRARY_VRFY = 103;
@@ -141,8 +151,20 @@ public class MediaScanner implements Handler.Callback {
MediaLibrary.notifyObserver();
break;
}
- case RPC_NOOP: {
- // just used to trigger the initial scan
+ case MSG_SCAN_FINISHED: {
+ if (mIsInitialScan) {
+ mIsInitialScan = false;
+ PlaylistBridge.importAndroidPlaylists(mContext);
+ toastMsg(R.string.media_library_import_ended);
+ }
+ break;
+ }
+ case RPC_KICKSTART: {
+ // a new scan was triggered: check if this is a 'initial / from scratch' scan
+ if (!mIsInitialScan && getSetScanMark(-1) == 0) {
+ mIsInitialScan = true;
+ toastMsg(R.string.media_library_import_started);
+ }
break;
}
case RPC_INSPECT_FILE: {
@@ -174,7 +196,7 @@ public class MediaScanner implements Handler.Callback {
if (message.what == MSG_SCAN_RPC && !mHandler.hasMessages(MSG_SCAN_RPC)) {
MediaScanPlan.Step step = mScanPlan.getNextStep();
if (step == null) {
- Log.v("VanillaMusic", "--- all scanners finished ---");
+ mHandler.sendEmptyMessage(MSG_SCAN_FINISHED);
} else {
Log.v("VanillaMusic", "--- starting scan of type "+step.msg);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SCAN_RPC, step.msg, 0, step.arg));
@@ -423,6 +445,17 @@ public class MediaScanner implements Handler.Callback {
return oldVal;
}
+ /**
+ * Creates a toast message
+ * Not sure if this should really be here - a callback to the
+ * observer would probably be nicer
+ *
+ * @param id the message id to display
+ */
+ private void toastMsg(int resId) {
+ Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show();
+ }
+
// MediaScanPlan describes how we are going to perform the media scan
class MediaScanPlan {
class Step {
diff --git a/src/ch/blinkenlights/android/medialibrary/PlaylistBridge.java b/src/ch/blinkenlights/android/medialibrary/PlaylistBridge.java
new file mode 100644
index 00000000..4a3a3793
--- /dev/null
+++ b/src/ch/blinkenlights/android/medialibrary/PlaylistBridge.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 Adrian Ulrich
+ *
+ * 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 .
+ */
+
+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 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);
+ }
+
+}