package com.vaguehope.onosendai.update;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.json.JSONException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import com.vaguehope.onosendai.C;
import com.vaguehope.onosendai.config.Column;
import com.vaguehope.onosendai.config.Config;
import com.vaguehope.onosendai.config.Prefs;
import com.vaguehope.onosendai.model.Meta;
import com.vaguehope.onosendai.model.MetaType;
import com.vaguehope.onosendai.model.PrefetchMode;
import com.vaguehope.onosendai.model.ScrollState;
import com.vaguehope.onosendai.model.Tweet;
import com.vaguehope.onosendai.provider.ProviderMgr;
import com.vaguehope.onosendai.storage.DbBindingService;
import com.vaguehope.onosendai.storage.DbInterface;
import com.vaguehope.onosendai.storage.DbInterface.Selection;
import com.vaguehope.onosendai.storage.TweetCursorReader;
import com.vaguehope.onosendai.ui.pref.FetchingPrefFragment;
import com.vaguehope.onosendai.util.BatteryHelper;
import com.vaguehope.onosendai.util.IoHelper;
import com.vaguehope.onosendai.util.LogWrapper;
import com.vaguehope.onosendai.util.NetHelper;
public abstract class AbstractBgFetch<D> extends DbBindingService {
public static final String ARG_COLUMN_IDS = "column_ids";
public static final String ARG_IS_MANUAL = "is_manual";
private static final LogWrapper LOG = new LogWrapper("ABF");
protected static void startServiceIfConfigured (final Class<? extends AbstractBgFetch<?>> cls, final String prefKey, final Context context, final Prefs prefs, final Collection<Column> columns, final boolean manual) {
final PrefetchMode prefetchMode = readPrefetchMode(prefs, prefKey);
if (prefetchMode == PrefetchMode.NO) {
return;
}
else if (prefetchMode == PrefetchMode.ALWAYS) {
startService(cls, context, columns, manual);
}
else if (prefetchMode == PrefetchMode.WIFI_ONLY) {
if (NetHelper.isWifi(context)) {
startService(cls, context, columns, manual);
}
else {
LOG.i("Not fetching; not on WiFi.");
}
}
else {
LOG.i("Not fetching; unknown mode: %s.", prefetchMode);
}
}
private static PrefetchMode readPrefetchMode (final Prefs prefs, final String prefKey) {
return PrefetchMode.parseValue(
prefs.getSharedPreferences().getString(
prefKey,
PrefetchMode.NO.getValue()));
}
private static void startService (final Class<? extends AbstractBgFetch<?>> cls, final Context context, final Collection<Column> columns, final boolean manual) {
final int[] columnIds = new int[columns.size()];
final Iterator<Column> columnsIttr = columns.iterator();
for (int i = 0; i < columnIds.length; i++) {
columnIds[i] = columnsIttr.next().getId();
}
final Intent intent = new Intent(context, cls);
intent.putExtra(ARG_COLUMN_IDS, columnIds);
intent.putExtra(ARG_IS_MANUAL, manual);
context.startService(intent);
}
private final boolean withInlineMediaOnly;
public AbstractBgFetch (final Class<? extends AbstractBgFetch<?>> cls, final boolean withInlineMediaOnly, final LogWrapper log) {
super(cls.getSimpleName(), log);
this.withInlineMediaOnly = withInlineMediaOnly;
}
@Override
protected void doWork (final Intent i) {
final int[] columnIds = i.getIntArrayExtra(ARG_COLUMN_IDS);
final boolean manual = i.getBooleanExtra(ARG_IS_MANUAL, false);
getLog().i("%s invoked (column_ids=%s, is_manual=%b).", getClass().getSimpleName(), Arrays.toString(columnIds), manual);
doWork(columnIds, manual);
}
private void doWork (final int[] columnIds, final boolean manual) {
if (NetHelper.connectionPresent(this)) {
fetchColumns(columnIds, manual);
}
else {
getLog().i("No connection, all fetching aborted.");
}
}
private void fetchColumns (final int[] columnIds, final boolean manual) {
final Prefs prefs = new Prefs(getBaseContext());
Config conf;
try {
conf = prefs.asConfig();
}
catch (final JSONException e) {
getLog().w("Can not parse conf: %s", e.toString());
return;
}
final Collection<Column> columns = new ArrayList<Column>();
for (final int colId : columnIds) {
columns.add(conf.getColumnById(colId));
}
if (!waitForDbReady()) return;
fetchIfBatteryOk(columns, manual, conf);
}
private void fetchIfBatteryOk (final Collection<Column> columnsToFetch, final boolean manual, final Config conf) {
final double batLimit = manual ? C.MIN_BAT_BG_FETCH_MANUAL : FetchingPrefFragment.readMinBatForUpdate(this);
final float bl = BatteryHelper.level(getApplicationContext());
if (bl < batLimit) {
getLog().i("Not fetching; battery %s < %s.", bl, batLimit);
return;
}
getLog().i("Fetching (bl=%s, m=%s) ...", bl, manual);
fetch(columnsToFetch, conf);
}
private void fetch (final Collection<Column> columnsToFetch, final Config conf) {
final long startTime = System.nanoTime();
final List<D> metas = new ArrayList<D>();
for (final Column col : columnsToFetch) {
metas.addAll(findToFetch(col, conf));
}
final int downloadCount = download(metas);
final long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
getLog().i("Fetched %d in %d millis.", downloadCount, durationMillis);
}
/**
* Ordered oldest (first to be scrolled up to) first.
*/
private List<D> findToFetch (final Column col, final Config conf) {
final DbInterface db = getDb();
final ScrollState scroll = db.getScroll(col.getId());
final Cursor cursor = db.getTweetsCursor(col.getId(), Selection.FILTERED, col.getExcludeColumnIds(), this.withInlineMediaOnly);
try {
final TweetCursorReader reader = new TweetCursorReader();
if (cursor != null && cursor.moveToFirst()) {
final List<D> metas = new ArrayList<D>();
do {
if (scroll != null && reader.readTime(cursor) < scroll.getUnreadTime()) break; // Stop gathering URLs at unread point.
readUrls(cursor, reader, col, conf, metas);
final List<Meta> quotedMetas = db.getTweetMetasOfType(reader.readUid(cursor), MetaType.QUOTED_SID);
if (quotedMetas != null) {
for (final Meta m : quotedMetas) {
if (m.getData() != null) {
final Tweet quotedTweet = db.getTweetDetails(m.getData());
if (quotedTweet != null) {
readUrls(quotedTweet, col, conf, metas);
}
else {
getLog().w("Quoted tweet not in DB: %s", m.getData());
}
}
}
}
}
while (cursor.moveToNext());
Collections.reverse(metas); // Fetch oldest first.
return metas;
}
return Collections.emptyList();
}
finally {
IoHelper.closeQuietly(cursor);
}
}
protected abstract void readUrls (final Cursor cursor, final TweetCursorReader reader, Column col, Config conf, final List<D> retMetas);
protected abstract void readUrls (final Tweet tweet, Column col, Config conf, final List<D> retMetas);
private int download (final List<D> metas) {
if (metas == null || metas.size() < 1) return 0;
final ProviderMgr provMgr = new ProviderMgr(getDb());
try {
return download(metas, provMgr);
}
finally {
provMgr.shutdown();
}
}
private int download (final List<D> metas, final ProviderMgr provMgr) {
final Map<String, Callable<?>> jobs = new LinkedHashMap<String, Callable<?>>();
makeJobs(metas, provMgr, jobs);
if (jobs.size() < 1) return 0;
final int poolSize = Math.min(jobs.size(), C.UPDATER_MAX_THREADS);
getLog().i("Downloading %s using %s threads.", jobs.size(), poolSize);
final ExecutorService ex = Executors.newFixedThreadPool(poolSize);
try {
final Map<String, Future<?>> futures = new LinkedHashMap<String, Future<?>>();
for (final Entry<String, Callable<?>> job : jobs.entrySet()) {
futures.put(job.getKey(), ex.submit(job.getValue()));
}
for (final Entry<String, Future<?>> future : futures.entrySet()) {
try {
future.getValue().get();
}
catch (final InterruptedException e) {
getLog().w("Error fetching '%s': %s %s", future.getKey(), e.getClass().getName(), e.toString());
}
catch (final ExecutionException e) {
getLog().w("Error fetching '%s': %s %s", future.getKey(), e.getClass().getName(), e.toString());
}
}
return futures.size();
}
finally {
ex.shutdownNow();
}
}
protected abstract void makeJobs (final List<D> metas, ProviderMgr provMgr, final Map<String, Callable<?>> jobs);
}