/*
* Copyright (C) 2013 jonas.oreland@gmail.com
*
* 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/>.
*/
package org.runnerup.feed;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.util.Log;
import org.runnerup.common.util.Constants;
import org.runnerup.db.DBHelper;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Observable;
import java.util.Set;
import java.util.TimeZone;
@TargetApi(Build.VERSION_CODES.FROYO)
public class FeedList extends Observable implements Constants {
static final int MAX_ITEMS = 50;
static final long TIME_MARGIN = 5 * 60; // 5 minutes
final SQLiteDatabase mDB;
List<ContentValues> list = new ArrayList<ContentValues>();
boolean filterDuplicates = true;
public FeedList(SQLiteDatabase db) {
mDB = db;
}
public void setFilterDuplicates(boolean val) {
filterDuplicates = val;
}
public boolean getFilterDuplicates() {
return filterDuplicates;
}
public void load() {
list.clear();
Cursor c = mDB.query(DB.FEED.TABLE, null, null, null, null, null, DB.FEED.START_TIME
+ " desc", Integer.toString(MAX_ITEMS));
if (c.moveToFirst()) {
do {
list.add(DBHelper.get(c));
} while (c.moveToNext());
}
c.close();
}
public void reset() {
mDB.execSQL("DELETE FROM " + DB.FEED.TABLE);
}
public void prune() {
if (list.size() >= MAX_ITEMS + 1) {
ContentValues tmp = list.get(MAX_ITEMS);
long start_time = tmp.getAsLong(DB.FEED.START_TIME);
mDB.execSQL("DELETE FROM " + DB.FEED.TABLE + " WHERE " + DB.FEED.START_TIME + " < "
+ start_time);
List<ContentValues> swap = list.subList(0, MAX_ITEMS);
list = swap;
}
}
public List<ContentValues> getList() {
return list;
}
public class FeedUpdater {
List<ContentValues> currList = null;
List<ContentValues> addList = null;
String synchronizer = null;
int added = 0;
int discarded = 0;
FeedUpdater() {
currList = new ArrayList<ContentValues>(list.size());
for (ContentValues c : list) { // initialize list with already
// present items
if (!isHeaderDate(c))
currList.add(c);
}
addList = new ArrayList<ContentValues>(list.size());
}
public void start(String synchronizerName) {
synchronizer = synchronizerName;
added = discarded = 0;
setChanged();
notifyObservers(synchronizerName);
}
// this method is called by different thread (not UI thread)
public void addAll(List<ContentValues> result) {
for (ContentValues c : result) {
add(c);
}
}
// this method is called by different thread (not UI thread)
public void add(ContentValues values) {
long startTime = values.getAsLong(DB.FEED.START_TIME);
long endTime = startTime;
if (values.containsKey(DB.FEED.DURATION))
endTime = startTime + values.getAsLong(DB.FEED.DURATION);
int endIndex = findEndIndex(startTime - TIME_MARGIN);
int startIndex = findStartIndex(endIndex, endTime + TIME_MARGIN);
for (int i = startIndex; i <= endIndex; i++) {
ContentValues c = currList.get(i);
if (match(values, c, filterDuplicates) == true) {
// Set already contains matching row...skip this
discarded++;
return;
}
}
added++;
addList.add(values); // no match, add this row
mDB.insert(DB.FEED.TABLE, null, values);
}
private int findEndIndex(long time) {
return currList.size() - 1;
}
private int findStartIndex(int startIndex, long l) {
return 0;
}
public void complete() {
if (addList.size() > 0) {
mergeLists(currList, addList);
list = currList;
setChanged();
notifyObservers(null);
}
prune();
Log.i(getClass().getSimpleName(), "FeedUpdater: " + synchronizer + ", added: " + added + ", discarded: "
+ discarded);
}
private void mergeLists(List<ContentValues> l1, List<ContentValues> l2) {
if (l2.isEmpty())
return;
l1.addAll(l2);
FeedList.sort(l1);
l2.clear();
}
}
public FeedUpdater getUpdater() {
return new FeedUpdater();
}
public static List<ContentValues> addHeaders(List<ContentValues> oldList) {
List<ContentValues> newList = new ArrayList<ContentValues>(oldList.size());
Calendar lastDate = Calendar.getInstance();
lastDate.setTimeInMillis(0);
Calendar tmp = Calendar.getInstance();
for (ContentValues anOldList : oldList) {
if (isHeaderDate(anOldList)) {
setDate(lastDate, anOldList);
} else {
setDate(tmp, anOldList);
if (compare(tmp, lastDate) != 0) {
newList.add(newHeaderDate(tmp.getTimeInMillis()));
lastDate.setTime(tmp.getTime());
}
newList.add(anOldList);
}
}
return newList;
}
private static int compare(Calendar c1, Calendar c2) {
int res = c1.get(Calendar.YEAR) - c2.get(Calendar.YEAR);
if (res == 0) {
res = c1.get(Calendar.DAY_OF_YEAR) - c2.get(Calendar.DAY_OF_YEAR);
}
return res;
}
private static ContentValues newHeaderDate(long time) {
ContentValues tmp = new ContentValues();
tmp.put(DB.FEED.START_TIME, time);
tmp.put(DB.FEED.FEED_TYPE, DB.FEED.FEED_TYPE_EVENT);
tmp.put(DB.FEED.FEED_SUBTYPE, DB.FEED.FEED_TYPE_EVENT_DATE_HEADER);
return tmp;
}
private static void setDate(Calendar lastDate, ContentValues tmp) {
lastDate.setTimeInMillis(tmp.getAsLong(DB.FEED.START_TIME));
lastDate.set(Calendar.HOUR, 0);
lastDate.set(Calendar.MINUTE, 0);
lastDate.set(Calendar.SECOND, 0);
lastDate.set(Calendar.MILLISECOND, 0);
lastDate.setTimeInMillis(lastDate.getTimeInMillis());
}
public static boolean isHeaderDate(ContentValues tmp) {
return tmp.getAsInteger(DB.FEED.FEED_TYPE) == DB.FEED.FEED_TYPE_EVENT &&
tmp.getAsInteger(DB.FEED.FEED_SUBTYPE) == DB.FEED.FEED_TYPE_EVENT_DATE_HEADER;
}
public static boolean isActivity(ContentValues tmp) {
return tmp.getAsInteger(DB.FEED.FEED_TYPE) == DB.FEED.FEED_TYPE_ACTIVITY;
}
public static boolean match(ContentValues c0, ContentValues c1, boolean filterDuplicates) {
boolean same_account = c0.getAsLong(DB.FEED.ACCOUNT_ID) == c1.getAsLong(DB.FEED.ACCOUNT_ID);
if (same_account) {
/**
* Always filter duplicates from same account
*/
if (c0.containsKey(DB.FEED.EXTERNAL_ID) && c1.containsKey(DB.FEED.EXTERNAL_ID)) {
if (c0.getAsString(DB.FEED.EXTERNAL_ID).contentEquals(
c1.getAsString(DB.FEED.EXTERNAL_ID)))
return true;
}
// Same account must be identical to match
// (this is a theoretical problem with upgrade...)
Set<Entry<String, Object>> v0 = c0.valueSet();
for (Entry<String, Object> val : v0) {
if ("_id".contentEquals(val.getKey()))
continue;
if (!c1.containsKey(val.getKey())) {
return false;
}
if (!val.getValue().toString().contentEquals(c1.getAsString(val.getKey())))
return false;
}
int i0 = c0.containsKey("_id") ? 1 : 0;
int i1 = c1.containsKey("_id") ? 1 : 0;
return (v0.size() - i0) == (c1.valueSet().size() - i1);
}
if (!filterDuplicates)
return false;
boolean print = false; // enable printout of match failure
String keys[] = {
DB.FEED.FEED_TYPE,
DB.FEED.FEED_SUBTYPE,
DB.FEED.FEED_TYPE_STRING,
DB.FEED.USER_FIRST_NAME,
DB.FEED.USER_LAST_NAME
};
for (String k : keys) {
if (c0.containsKey(k) && c1.containsKey(k))
if (!c0.getAsString(k).equalsIgnoreCase(c1.getAsString(k))) {
if (print)
Log.i("FeedList", "fail at " + k + " c0: " + c0.getAsString(k) + ", c1: "
+ c1.getAsString(k));
return false;
}
}
if (overlaps(c0, c1, print)) {
return true;
}
return false;
}
private static boolean overlaps(ContentValues c0, ContentValues c1, boolean print) {
long t0s = c0.getAsLong(DB.FEED.START_TIME) / 1000; // ms => s
long t0e = t0s;
if (c0.containsKey(DB.FEED.DURATION))
t0e += c0.getAsLong(DB.FEED.DURATION);
long t1s = c1.getAsLong(DB.FEED.START_TIME) / 1000; // ms => s
long t1e = t1s;
if (c1.containsKey(DB.FEED.DURATION))
t1e += c1.getAsLong(DB.FEED.DURATION);
if (t1s >= t0s && t1s <= t0e)
return true;
if (t1e >= t0s && t1e <= t0e)
return true;
if (t1s <= t0s && t1e >= t0e)
return true;
boolean broken0 = c0.containsKey(DB.FEED.FLAGS)
&& c0.getAsString(DB.FEED.FLAGS).contains("brokenStartTime");
boolean broken1 = c1.containsKey(DB.FEED.FLAGS)
&& c1.getAsString(DB.FEED.FLAGS).contains("brokenStartTime");
if (broken0 || broken1) {
/**
* check if same day, if so compare distance/duration instead
*/
Calendar d0 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Calendar d1 = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
d0.setTimeInMillis(t0s * 1000);
d1.setTimeInMillis(t1s * 1000);
if ((d0.get(Calendar.YEAR) != d1.get(Calendar.YEAR)) ||
(d0.get(Calendar.DAY_OF_YEAR) != d1.get(Calendar.DAY_OF_YEAR))) {
if (print)
Log.i("FeedList", "fail at d0: " + d0.toString() + ", d1: " + d1.toString());
return false;
}
int dur_match = 0;
if (c0.containsKey(DB.FEED.DURATION) && c1.containsKey(DB.FEED.DURATION)) {
double dur0 = c0.getAsDouble(DB.FEED.DURATION);
double dur1 = c1.getAsDouble(DB.FEED.DURATION);
if (dur0 != 0 && dur1 != 0) {
double pct = Math.abs((dur0 / dur1) - 1);
if (pct > 0.1) {
if (print)
Log.i("FeedList", "fail at dur0: " + dur0 + "dur1: " + dur1
+ " => pct: " + pct);
return false;
}
dur_match = 2;
}
} else if (!c0.containsKey(DB.FEED.DURATION) && !c1.containsKey(DB.FEED.DURATION)) {
dur_match = 1;
}
int dis_match = 0;
if (c0.containsKey(DB.FEED.DISTANCE) && c1.containsKey(DB.FEED.DISTANCE)) {
double dur0 = c0.getAsDouble(DB.FEED.DISTANCE);
double dur1 = c1.getAsDouble(DB.FEED.DISTANCE);
if (dur0 != 0 && dur1 != 0) {
double pct = Math.abs((dur0 / dur1) - 1);
if (pct > 0.1) {
if (print)
Log.i("FeedList", "fail at dis0: " + dur0 + "dis1: " + dur1
+ " => pct: " + pct);
return false;
}
dis_match = 2;
}
} else if (!c0.containsKey(DB.FEED.DISTANCE) && !c1.containsKey(DB.FEED.DISTANCE)) {
dis_match = 1;
}
if (dis_match + dur_match >= 3) {
return true;
}
Log.i("FeedList", "dur_match: " + dur_match + ", dis_match: " + dis_match + ", c0: "
+ FeedList.toString(c0) + ", c1: " + FeedList.toString(c1));
}
return false;
}
public static String toString(ContentValues tmp) {
StringBuilder buf = new StringBuilder();
buf.append("[ ");
boolean first = true;
for (Entry<String, Object> e : tmp.valueSet()) {
if (!first) {
buf.append(", ");
}
first = false;
buf.append(e.getKey());
buf.append(":");
buf.append(e.getValue());
}
if (!first)
buf.append(" ");
buf.append("]");
return buf.toString();
}
public static void sort(List<ContentValues> list) {
java.util.Collections.sort(list, new Comparator<ContentValues>() {
@Override
public int compare(ContentValues lhs, ContentValues rhs) {
long t1 = lhs.getAsLong(DB.FEED.START_TIME);
long t2 = rhs.getAsLong(DB.FEED.START_TIME);
if (t1 < t2) {
return +1;
} else if (t1 > t2) {
return -1;
}
return 0;
}
});
}
}