implement media scanner UI

This commit is contained in:
Adrian Ulrich 2016-12-27 21:44:42 +01:00
parent 447059150e
commit 08f79a84ae
10 changed files with 282 additions and 166 deletions

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="5dp"
android:orientation="vertical" >
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:textColor="?overlay_foreground_color"
android:text="@string/media_scan_header" />
<CheckBox android:id="@+id/media_scan_full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/media_scan_full" />
<CheckBox android:id="@+id/media_scan_drop_db"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/media_scan_drop_db" />
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/media_scan_start">
<requestFocus />
</Button>
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:textColor="?overlay_foreground_color"
android:text="@string/media_statistics" />
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stretchColumns="1">
<TableRow>
<TextView
android:text="@string/media_stats_songs"
android:padding="3dip" />
<TextView
android:id="@+id/media_stats_songs"
android:text="@string/empty"
android:gravity="right"
android:padding="3dip" />
</TableRow>
<TableRow>
<TextView
android:text="@string/media_stats_playtime"
android:padding="3dip" />
<TextView
android:text="@string/empty"
android:id="@+id/media_stats_playtime"
android:gravity="right"
android:padding="3dip" />
</TableRow>
</TableLayout>
<TextView
android:id="@+id/media_stats_progress"
android:paddingTop="24dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/empty" />
</LinearLayout>

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2013-2014 Jeremy Erickson
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 2 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.
-->
<!--
Copied from SD Scanner's layout/main.xml with minor changes
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingRight="16dp"
android:paddingLeft="16dp"
android:paddingBottom="16dp"
android:orientation="vertical" >
<TextView
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/sdscan_help" />
<Button
android:id="@+id/start_button"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_gravity="center"
android:text="@string/button_start">
<requestFocus />
</Button>
<TextView
android:id="@+id/progress_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="10000"
android:text="@string/progress_unstarted_label" />
</LinearLayout>

View File

@ -303,21 +303,14 @@ THE SOFTWARE.
<string name="sendto">Send to…</string>
<string name="no_receiving_apps">No receiving apps found for this media type!</string>
<!-- SD Scanner -->
<string name="sdscan_help">Starting a rescan causes Vanilla Music to trigger a full rebuild of the media database.</string>
<string name="button_start">Start Rescan</string>
<string name="database_proc">Examined</string>
<string name="delete_proc">Removed reference to</string>
<string name="db_label">Will also check existing media database for updated or deleted files.</string>
<string name="db_error_failure">Encountered error reading media database, and might miss updated or deleted files or rescan up-to-date files.</string>
<string name="db_error_recovered">Encountered error reading media database, but recovered.</string>
<string name="db_error_retrying">Encountered error reading media database. Retrying in 1 second...</string>
<string name="final_proc">Processed</string>
<string name="progress_completed_label">Completed, ready to start another scan.</string>
<string name="progress_error_bad_path_label">Scan failed: bad path specified for new file search.</string>
<string name="progress_filelist_label">Preparing initial list of files...</string>
<string name="progress_database_label">Querying database...</string>
<string name="progress_unstarted_label">Not yet started.</string>
<string name="skipping_folder_label">Encountered an error and skipping</string>
<string name="sdscanner">SD Scanner</string>
<string name="media_library">Media library</string>
<string name="media_scan_header">Media scanner</string>
<string name="media_scan_full">Scan full filesystem (Slow)</string>
<string name="media_scan_drop_db">Flush media database (Dangerzone!)</string>
<string name="media_scan_start">Start scan</string>
<string name="media_statistics">Statistics</string>
<string name="media_stats_songs">Number of songs</string>
<string name="media_stats_playtime">Play time (Hours)</string>
<string name="media_stats_progress">Scan progress</string>
</resources>

View File

@ -24,6 +24,9 @@ THE SOFTWARE.
<header
android:fragment="ch.blinkenlights.android.vanilla.PreferencesActivity$AudioFragment"
android:title="@string/audio" />
<header
android:fragment="ch.blinkenlights.android.vanilla.PreferencesMediaLibrary"
android:title="@string/media_library" />
<header
android:fragment="ch.blinkenlights.android.vanilla.PreferencesActivity$PlaybackFragment"
android:title="@string/playback_screen" />

View File

@ -24,9 +24,6 @@ THE SOFTWARE.
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:vanilla="http://schemas.android.com/apk/res/ch.blinkenlights.android.vanilla"
android:persistent="true">
<PreferenceScreen
android:fragment="ch.blinkenlights.android.vanilla.SDScannerFragment"
android:title="@string/sdscanner" />
<CheckBoxPreference
android:key="disable_lockscreen"
android:title="@string/disable_lockscreen_title"

View File

@ -86,8 +86,7 @@ public class MediaLibrary {
public static void scanLibrary(Context context, boolean forceFull, boolean drop) {
MediaLibraryBackend backend = getBackend(context); // also initialized sScanner
if (drop) {
backend.execSQL("DELETE FROM "+MediaLibrary.TABLE_SONGS);
forceFull = true;
sScanner.flushDatabase();
// fixme: should clean orphaned AFTER scan finished
}
@ -109,7 +108,7 @@ public class MediaLibrary {
MediaScanner.MediaScanPlan.Statistics stats = sScanner.getScanStatistics();
String msg = null;
if (stats.lastFile != null)
msg = "seen files = "+stats.seen+", changes made = "+stats.changed+", currently scanning = "+stats.lastFile;
msg = stats.lastFile+" ("+stats.changed+" / "+stats.seen+")";
return msg;
}
@ -161,7 +160,7 @@ public class MediaLibrary {
int rows = getBackend(context).delete(TABLE_SONGS, SongColumns._ID+"="+id, null);
if (rows > 0) {
getBackend(context).cleanOrphanedEntries();
getBackend(context).cleanOrphanedEntries(true);
notifyObserver();
}
return rows;

View File

@ -151,15 +151,20 @@ public class MediaLibraryBackend extends SQLiteOpenHelper {
/**
* Purges orphaned entries from the media library
*
* @param purgeUserData also delete user data, such as playlists if true
*/
void cleanOrphanedEntries() {
void cleanOrphanedEntries(boolean purgeUserData) {
SQLiteDatabase dbh = getWritableDatabase();
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_ALBUMS+" WHERE "+MediaLibrary.AlbumColumns._ID+" NOT IN (SELECT "+MediaLibrary.SongColumns.ALBUM_ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_GENRES_SONGS+" WHERE "+MediaLibrary.GenreSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_GENRES+" WHERE "+MediaLibrary.GenreColumns._ID+" NOT IN (SELECT "+MediaLibrary.GenreSongColumns._GENRE_ID+" FROM "+MediaLibrary.TABLE_GENRES_SONGS+");");
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+" WHERE "+MediaLibrary.ContributorSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_CONTRIBUTORS+" WHERE "+MediaLibrary.ContributorColumns._ID+" NOT IN (SELECT "+MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID+" FROM "+MediaLibrary.TABLE_CONTRIBUTORS_SONGS+");");
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_PLAYLISTS_SONGS+" WHERE "+MediaLibrary.PlaylistSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
if (purgeUserData) {
dbh.execSQL("DELETE FROM "+MediaLibrary.TABLE_PLAYLISTS_SONGS+" WHERE "+MediaLibrary.PlaylistSongColumns.SONG_ID+" NOT IN (SELECT "+MediaLibrary.SongColumns._ID+" FROM "+MediaLibrary.TABLE_SONGS+");");
}
}
/**

View File

@ -77,6 +77,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));
}
/**
@ -103,6 +104,15 @@ public class MediaScanner implements Handler.Callback {
}
}
/**
* Drops the media library
*/
public void flushDatabase() {
mBackend.delete(MediaLibrary.TABLE_SONGS, null, null);
mBackend.cleanOrphanedEntries(false); // -> keep playlists
getSetScanMark(0);
}
/**
* Returns some scan statistics
*
@ -373,7 +383,7 @@ public class MediaScanner implements Handler.Callback {
if (needsCleanup)
mBackend.cleanOrphanedEntries();
mBackend.cleanOrphanedEntries(true);
Log.v("VanillaMusic", "MediaScanner: inserted "+path);
return (needsInsert || needsCleanup);

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2016 Xiao Bao Clark
* 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 2 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.
*/
package ch.blinkenlights.android.vanilla;
import ch.blinkenlights.android.medialibrary.MediaLibrary;
import android.app.Fragment;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;
import android.util.Log;
public class PreferencesMediaLibrary extends Fragment
{
/**
* The ugly timer which fires every 200ms
*/
private Timer mTimer;
/**
* Our start button
*/
private View mStartButton;
/**
* The debug / progress text describing the scan status
*/
private TextView mProgress;
/**
* The number of songs on this device
*/;
private TextView mStatsSongs;
/**
* The number of hours of music we have
*/
private TextView mStatsPlaytime;
/**
* Checkbox for full scan
*/
private CheckBox mFullScanCheck;
/**
* Checkbox for drop
*/
private CheckBox mDropDbCheck;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.medialibrary_preferences, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mStartButton = (View)view.findViewById(R.id.start_button);
mProgress = (TextView)view.findViewById(R.id.media_stats_progress);
mStatsSongs = (TextView)view.findViewById(R.id.media_stats_songs);
mStatsPlaytime = (TextView)view.findViewById(R.id.media_stats_playtime);
mFullScanCheck = (CheckBox)view.findViewById(R.id.media_scan_full);
mDropDbCheck = (CheckBox)view.findViewById(R.id.media_scan_drop_db);
mStartButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startButtonPressed(v);
}
});
}
@Override
public void onResume() {
super.onResume();
mTimer = new Timer();
// Yep: its as ugly as it seems: we are POLLING
// the database.
mTimer.scheduleAtFixedRate((new TimerTask() {
@Override
public void run() {
getActivity().runOnUiThread(new Runnable(){
public void run() {
updateProgress();
}
});
}}), 0, 200);
}
@Override
public void onPause() {
super.onPause();
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
/**
* Updates the view of this fragment with current information
*/
private void updateProgress() {
Context context = getActivity();
String scanText = MediaLibrary.describeScanProgress(getActivity());
mProgress.setText(scanText);
mStartButton.setEnabled(scanText == null);
Integer songCount = MediaLibrary.getLibrarySize(context);
mStatsSongs.setText(songCount.toString());
Float playtime = calculateDuration(context) / 3600000F;
mStatsPlaytime.setText(playtime.toString());
}
/**
* Queries the media library and calculates the total amount of playtime in ms
*
* @param context the context to use
* @return the play time of the library in ms
*/
public int calculateDuration(Context context) {
int duration = 0;
Cursor cursor = MediaLibrary.queryLibrary(context, MediaLibrary.TABLE_SONGS, new String[]{"SUM("+MediaLibrary.SongColumns.DURATION+")"}, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
duration = cursor.getInt(0);
}
cursor.close();
}
return duration;
}
/**
* Called when the user hits the start button
*
* @param view the view which was pressed
*/
public void startButtonPressed(View view) {
MediaLibrary.scanLibrary(getActivity(), mFullScanCheck.isChecked(), mDropDbCheck.isChecked());
updateProgress();
}
}

View File

@ -1,92 +0,0 @@
/*
* Copyright (C) 2016 Xiao Bao Clark
* 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 2 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.
*/
package ch.blinkenlights.android.vanilla;
import ch.blinkenlights.android.medialibrary.MediaLibrary;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;
import android.util.Log;
public class SDScannerFragment extends Fragment
{
private Timer mTimer;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.sdscanner_fragment, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.findViewById(R.id.start_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startButtonPressed(v);
}
});
}
@Override
public void onResume() {
super.onResume();
Log.v("VanillaMusic", "onResume! "+mTimer);
mTimer = new Timer();
mTimer.scheduleAtFixedRate((new TimerTask() {
@Override
public void run() {
getActivity().runOnUiThread(new Runnable(){
public void run() {
updateProgress();
}
});
}}), 0, 120);
}
@Override
public void onPause() {
super.onPause();
Log.v("VanillaMusic", "onPause "+mTimer);
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
private void updateProgress() {
View button = getActivity().findViewById(R.id.start_button);
TextView progress = (TextView)getActivity().findViewById(R.id.progress_label);
String scanText = MediaLibrary.describeScanProgress(getActivity());
progress.setText(scanText);
button.setEnabled(scanText == null);
}
public void startButtonPressed(View view) {
MediaLibrary.scanLibrary(getActivity(), true, false);
updateProgress();
}
}