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); + } + +}