Further speed up playlist processing

Previously when processing playlists, the entire audio table would be read for
every line in a playlist file. While there would be only one query, a lot of
data was being moved from sqlite to java over and over again, and if the data
didn't all fit in a CursorWindow, additional queries would be done under the
hood.
With this change, playlists are first cached in memory. Then the audio table
is queried, and for every row in the audio table, the best match from the
playlist cache is found. This way the audio table is only traversed once,
so each row is only fetched once. Once the entire audio table has been read,
the in-memory playlist cache contains the best matching entry for each line,
and the playlists are written out to the database. Currently, the audio table
is traversed once for each playlist. This could be further optimized in the
future by processing all playlists at the same time.

b/6346786

Change-Id: Iead3f9ae838d600d085e8e6d3c4874d42314468e
This commit is contained in:
Marco Nelissen
2012-04-24 16:54:59 -07:00
parent d7a2a428db
commit 0b718392c1

View File

@ -31,17 +31,16 @@ import android.graphics.BitmapFactory;
import android.mtp.MtpConstants;
import android.net.Uri;
import android.os.Environment;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.Settings;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.Playlists;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.Audio.Playlists;
import android.provider.Settings;
import android.sax.Element;
import android.sax.ElementListener;
import android.sax.RootElement;
@ -56,10 +55,8 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import libcore.io.ErrnoException;
@ -372,6 +369,14 @@ public class MediaScanner
}
}
private static class PlaylistEntry {
String path;
long bestmatchid;
int bestmatchlevel;
}
private ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<PlaylistEntry>();
private MediaInserter mMediaInserter;
private ArrayList<FileEntry> mPlayLists;
@ -1492,93 +1497,83 @@ public class MediaScanner
return result;
}
private boolean addPlayListEntry(String entry, String playListDirectory,
Uri uri, ContentValues values, int index, Cursor fileList) {
private boolean matchEntries(long rowId, String data) {
int len = mPlaylistEntries.size();
boolean done = true;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
if (entry.bestmatchlevel == Integer.MAX_VALUE) {
continue; // this entry has been matched already
}
done = false;
if (data.equalsIgnoreCase(entry.path)) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = Integer.MAX_VALUE;
continue; // no need for path matching
}
int matchLength = matchPaths(data, entry.path);
if (matchLength > entry.bestmatchlevel) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = matchLength;
}
}
return done;
}
private void cachePlaylistEntry(String line, String playListDirectory) {
PlaylistEntry entry = new PlaylistEntry();
// watch for trailing whitespace
int entryLength = entry.length();
while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
int entryLength = line.length();
while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
// path should be longer than 3 characters.
// avoid index out of bounds errors below by returning here.
if (entryLength < 3) return false;
if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
if (entryLength < 3) return;
if (entryLength < line.length()) line = line.substring(0, entryLength);
// does entry appear to be an absolute path?
// look for Unix or DOS absolute paths
char ch1 = entry.charAt(0);
char ch1 = line.charAt(0);
boolean fullPath = (ch1 == '/' ||
(Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
(Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
// if we have a relative path, combine entry with playListDirectory
if (!fullPath)
entry = playListDirectory + entry;
line = playListDirectory + line;
entry.path = line;
//FIXME - should we look for "../" within the path?
// best matching MediaFile for the play list entry
FileEntry bestMatch = null;
mPlaylistEntries.add(entry);
}
// number of rightmost file/directory names for bestMatch
int bestMatchLength = 0;
if (fileList != null) {
int count = fileList.getCount();
// Backing up a little in the cursor helps when the files in the
// playlist are not in the same order as they are in the database
// but are still close.
fileList.move(-1000);
while(--count >= 0) {
if (!fileList.moveToNext()) {
fileList.moveToFirst();
}
long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String path = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
int format = fileList.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
long lastModified = fileList.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
if (path.equalsIgnoreCase(entry)) {
bestMatch = new FileEntry(rowId, path, lastModified, format);
break; // don't bother continuing search
}
int matchLength = matchPaths(path, entry);
if (matchLength > bestMatchLength) {
bestMatch = new FileEntry(rowId, path, lastModified, format);
bestMatchLength = matchLength;
}
private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
fileList.moveToPosition(-1);
while (fileList.moveToNext()) {
long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
if (matchEntries(rowId, data)) {
break;
}
}
if (bestMatch == null) {
return false;
}
try {
// check rowid is set. Rowid may be missing if it is inserted by bulkInsert().
if (bestMatch.mRowId == 0) {
Cursor c = mMediaProvider.query(mAudioUri, ID_PROJECTION,
MediaStore.Files.FileColumns.DATA + "=?",
new String[] { bestMatch.mPath }, null, null);
if (c != null) {
if (c.moveToNext()) {
bestMatch.mRowId = c.getLong(0);
}
c.close();
}
if (bestMatch.mRowId == 0) {
return false;
int len = mPlaylistEntries.size();
int index = 0;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
if (entry.bestmatchlevel > 0) {
try {
values.clear();
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
mMediaProvider.insert(playlistUri, values);
index++;
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
return;
}
}
// OK, now we are ready to add this to the database
values.clear();
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
mMediaProvider.insert(uri, values);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
return false;
}
return true;
mPlaylistEntries.clear();
}
private void processM3uPlayList(String path, String playListDirectory, Uri uri,
@ -1590,16 +1585,16 @@ public class MediaScanner
reader = new BufferedReader(
new InputStreamReader(new FileInputStream(f)), 8192);
String line = reader.readLine();
int index = 0;
mPlaylistEntries.clear();
while (line != null) {
// ignore comment lines, which begin with '#'
if (line.length() > 0 && line.charAt(0) != '#') {
values.clear();
if (addPlayListEntry(line, playListDirectory, uri, values, index, fileList))
index++;
cachePlaylistEntry(line, playListDirectory);
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
@ -1622,20 +1617,19 @@ public class MediaScanner
reader = new BufferedReader(
new InputStreamReader(new FileInputStream(f)), 8192);
String line = reader.readLine();
int index = 0;
mPlaylistEntries.clear();
while (line != null) {
// ignore comment lines, which begin with '#'
if (line.startsWith("File")) {
int equals = line.indexOf('=');
if (equals > 0) {
values.clear();
if (addPlayListEntry(line.substring(equals + 1), playListDirectory,
uri, values, index, fileList))
index++;
cachePlaylistEntry(line, playListDirectory);
}
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
@ -1653,15 +1647,9 @@ public class MediaScanner
final ContentHandler handler;
String playListDirectory;
Uri uri;
Cursor fileList;
ContentValues values = new ContentValues();
int index = 0;
public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
this.playListDirectory = playListDirectory;
this.uri = uri;
this.fileList = fileList;
RootElement root = new RootElement("smil");
Element body = root.getChild("body");
@ -1676,13 +1664,11 @@ public class MediaScanner
public void start(Attributes attributes) {
String path = attributes.getValue("", "src");
if (path != null) {
values.clear();
if (addPlayListEntry(path, playListDirectory, uri, values, index, fileList)) {
index++;
}
cachePlaylistEntry(path, playListDirectory);
}
}
@Override
public void end() {
}
@ -1692,15 +1678,18 @@ public class MediaScanner
}
private void processWplPlayList(String path, String playListDirectory, Uri uri,
Cursor fileList) {
ContentValues values, Cursor fileList) {
FileInputStream fis = null;
try {
File f = new File(path);
if (f.exists()) {
fis = new FileInputStream(f);
mPlaylistEntries.clear();
Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
new WplHandler(playListDirectory, uri, fileList).getContentHandler());
processCachedPlaylist(fileList, values, uri);
}
} catch (SAXException e) {
e.printStackTrace();
@ -1762,7 +1751,7 @@ public class MediaScanner
} else if (fileType == MediaFile.FILE_TYPE_PLS) {
processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
} else if (fileType == MediaFile.FILE_TYPE_WPL) {
processWplPlayList(path, playListDirectory, membersUri, fileList);
processWplPlayList(path, playListDirectory, membersUri, values, fileList);
}
}
@ -1800,7 +1789,7 @@ public class MediaScanner
private native final void native_finalize();
/**
* Releases resouces associated with this MediaScanner object.
* Releases resources associated with this MediaScanner object.
* It is considered good practice to call this method when
* one is done using the MediaScanner object. After this method
* is called, the MediaScanner object can no longer be used.