/*******************************************************************************
* This file is part of RedReader.
*
* RedReader 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.
*
* RedReader 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 RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.quantumbadger.redreader.cache;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.quantumbadger.redreader.activities.BugReportActivity;
import org.quantumbadger.redreader.common.RRTime;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.UUID;
final class CacheDbManager extends SQLiteOpenHelper {
private static final String CACHE_DB_FILENAME = "cache.db",
TABLE = "web",
FIELD_URL = "url",
FIELD_ID = "id",
FIELD_TIMESTAMP = "timestamp",
FIELD_SESSION = "session",
FIELD_USER = "user",
FIELD_STATUS = "status",
FIELD_TYPE = "type",
FIELD_MIMETYPE = "mimetype";
private static final int STATUS_MOVING = 1, STATUS_DONE = 2;
private static final int CACHE_DB_VERSION = 1;
private final Context context;
CacheDbManager(final Context context) {
super(context, CACHE_DB_FILENAME, null, CACHE_DB_VERSION);
this.context = context;
}
@Override
public void onCreate(final SQLiteDatabase db) {
final String queryString = String.format(
"CREATE TABLE %s (" +
"%s INTEGER PRIMARY KEY AUTOINCREMENT," +
"%s TEXT NOT NULL," +
"%s TEXT NOT NULL," +
"%s TEXT NOT NULL," +
"%s INTEGER," +
"%s INTEGER," +
"%s INTEGER," +
"%s TEXT," +
"UNIQUE (%s, %s, %s) ON CONFLICT REPLACE)",
TABLE,
FIELD_ID,
FIELD_URL,
FIELD_USER,
FIELD_SESSION,
FIELD_TIMESTAMP,
FIELD_STATUS,
FIELD_TYPE,
FIELD_MIMETYPE,
FIELD_USER, FIELD_URL, FIELD_SESSION);
db.execSQL(queryString);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
throw new RuntimeException("Attempt to upgrade database in first version of the app!");
}
synchronized LinkedList<CacheEntry> select(final URI url, final String user, final UUID session) {
final String[] fields = {FIELD_ID, FIELD_URL, FIELD_USER, FIELD_SESSION, FIELD_TIMESTAMP, FIELD_STATUS, FIELD_TYPE, FIELD_MIMETYPE};
final SQLiteDatabase db = this.getReadableDatabase();
final String queryString;
final String[] queryParams;
if(session == null) {
queryString = String.format(Locale.US, "%s=%d AND %s=? AND %s=?", FIELD_STATUS, STATUS_DONE, FIELD_URL, FIELD_USER);
queryParams = new String[] {url.toString(), user};
} else {
queryString = String.format(Locale.US, "%s=%d AND %s=? AND %s=? AND %s=?", FIELD_STATUS, STATUS_DONE, FIELD_URL, FIELD_USER, FIELD_SESSION);
queryParams = new String[] {url.toString(), user, session.toString()};
}
final Cursor cursor = db.query(TABLE, fields, queryString, queryParams, null, null, FIELD_TIMESTAMP + " DESC");
final LinkedList<CacheEntry> result = new LinkedList<>();
// TODO can this even happen?
if (cursor == null) {
BugReportActivity.handleGlobalError(context, "Cursor was null after query");
return null;
}
while(cursor.moveToNext()) {
result.add(new CacheEntry(cursor));
}
cursor.close();
return result;
}
synchronized long newEntry(final CacheRequest request, final UUID session, final String mimetype) throws IOException {
if(session == null) {
throw new RuntimeException("No session to write");
}
final SQLiteDatabase db = this.getWritableDatabase();
final ContentValues row = new ContentValues();
row.put(FIELD_URL, request.url.toString());
row.put(FIELD_USER, request.user.username);
row.put(FIELD_SESSION, session.toString());
row.put(FIELD_TYPE, request.fileType);
row.put(FIELD_STATUS, STATUS_MOVING);
row.put(FIELD_TIMESTAMP, RRTime.utcCurrentTimeMillis());
row.put(FIELD_MIMETYPE, mimetype);
final long result = db.insert(TABLE, null, row);
if(result < 0) throw new IOException("DB insert failed");
return result;
}
synchronized void setEntryDone(final long id) {
final SQLiteDatabase db = this.getWritableDatabase();
final ContentValues row = new ContentValues();
row.put(FIELD_STATUS, STATUS_DONE);
db.update(TABLE, row, FIELD_ID + "=?", new String[] {String.valueOf(id)});
}
synchronized int delete(final long id) {
final SQLiteDatabase db = this.getWritableDatabase();
return db.delete(TABLE, FIELD_ID + "=?", new String[] {String.valueOf(id)});
}
protected synchronized int deleteAllBeforeTimestamp(final long timestamp) {
final SQLiteDatabase db = this.getWritableDatabase();
return db.delete(TABLE, FIELD_TIMESTAMP + "<?", new String[] {String.valueOf(timestamp)});
}
public synchronized ArrayList<Long> getFilesToPrune(HashSet<Long> currentFiles, final HashMap<Integer, Long> maxAge, final long defaultMaxAge) {
final SQLiteDatabase db = this.getWritableDatabase();
final long currentTime = RRTime.utcCurrentTimeMillis();
final Cursor cursor = db.query(TABLE, new String[] {FIELD_ID, FIELD_TIMESTAMP, FIELD_TYPE}, null, null, null, null, null, null);
final HashSet<Long> currentEntries = new HashSet<>();
final ArrayList<Long> entriesToDelete = new ArrayList<>();
final ArrayList<Long> filesToDelete = new ArrayList<>(32);
while(cursor.moveToNext()) {
final long id = cursor.getLong(0);
final long timestamp = cursor.getLong(1);
final int type = cursor.getInt(2);
final long pruneIfBeforeMs;
if(maxAge.containsKey(type)) {
pruneIfBeforeMs = currentTime - maxAge.get(type);
} else {
Log.e("RR DEBUG cache", "Using default age! Filetype " + type);
pruneIfBeforeMs = currentTime - defaultMaxAge;
}
if(!currentFiles.contains(id)) {
entriesToDelete.add(id);
//Log.i("RR DEBUG cache", "DELETED ENTRY " + id + "(type " + type + ") since it had no matching file");
} else if(timestamp < pruneIfBeforeMs) {
entriesToDelete.add(id);
filesToDelete.add(id);
//Log.i("RR DEBUG cache", "DELETED ENTRY " + id + "(type " + type + ") since was too old");
} else {
currentEntries.add(id);
}
}
for(final long id : currentFiles) {
if(!currentEntries.contains(id)) {
filesToDelete.add(id);
//Log.i("RR DEBUG cache", "DELETED FILE " + id + " since it had no matching entry");
}
}
if(!entriesToDelete.isEmpty()) {
final StringBuilder query = new StringBuilder(String.format(Locale.US, "DELETE FROM %s WHERE %s IN (", TABLE, FIELD_ID));
query.append(entriesToDelete.remove(entriesToDelete.size() - 1));
for(final long id : entriesToDelete) {
query.append(",").append(id);
if(query.length() > 512 * 1024) break;
}
query.append(')');
db.execSQL(query.toString());
}
cursor.close();
return filesToDelete;
}
public synchronized void emptyTheWholeCache() {
final SQLiteDatabase db = this.getWritableDatabase();
db.execSQL(String.format(Locale.US, "DELETE FROM %s", TABLE));
}
}