package com.vaguehope.onosendai.update;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
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.Intent;
import com.vaguehope.onosendai.C;
import com.vaguehope.onosendai.config.Account;
import com.vaguehope.onosendai.config.Column;
import com.vaguehope.onosendai.config.ColumnFeed;
import com.vaguehope.onosendai.config.Config;
import com.vaguehope.onosendai.config.Prefs;
import com.vaguehope.onosendai.model.Filters;
import com.vaguehope.onosendai.notifications.Notifications;
import com.vaguehope.onosendai.provider.ProviderMgr;
import com.vaguehope.onosendai.storage.DbBindingService;
import com.vaguehope.onosendai.storage.DbInterface.ColumnState;
import com.vaguehope.onosendai.util.LogWrapper;
import com.vaguehope.onosendai.util.NetHelper;
public class UpdateService extends DbBindingService {
public static final String ARG_COLUMN_IDS = "column_ids";
public static final String ARG_IS_MANUAL = "is_manual";
protected static final LogWrapper LOG = new LogWrapper("US");
public UpdateService () {
super("OnosendaiUpdateService", LOG);
}
@Override
protected void doWork (final Intent i) {
final int[] columnIds = i.getIntArrayExtra(ARG_COLUMN_IDS);
final boolean manual = i.getBooleanExtra(ARG_IS_MANUAL, false);
LOG.i("UpdateService invoked (column_ids=%s, is_manual=%b).", Arrays.toString(columnIds), manual);
if (!fetchIfConnected(columnIds, manual) && columnIds != null) {
for (final int columnId : columnIds) {
getDb().notifyTwListenersColumnState(columnId, ColumnState.NOT_STARTED);
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private boolean fetchIfConnected (final int[] columnIds, final boolean manual) {
if (NetHelper.connectionPresent(this)) {
return fetchColumns(columnIds, manual);
}
LOG.i("No connection, all updating aborted.");
return false;
}
private boolean fetchColumns (final int[] columnIds, final boolean manual) {
final Prefs prefs = new Prefs(getBaseContext());
final Config conf;
try {
conf = prefs.asConfig();
}
catch (final JSONException e) {
LOG.w("Can not update: %s", e.toString());
return false;
}
final UpdateRequest req = new UpdateRequest(columnIds, manual, new Filters(prefs.readFilters()));
if (!waitForDbReady()) return false;
final Collection<Column> columnsFetched;
final ProviderMgr providerMgr = new ProviderMgr(getDb());
try {
columnsFetched = fetchColumns(conf, req, providerMgr);
}
finally {
providerMgr.shutdown();
}
HosakaSyncService.startServiceIfConfigured(this, conf, columnIds);
FetchPictureService.startServiceIfConfigured(this, prefs, columnsFetched, manual);
FetchLinkService.startServiceIfConfigured(this, prefs, columnsFetched, manual);
return true;
}
private Collection<Column> fetchColumns (final Config conf, final UpdateRequest req, final ProviderMgr providerMgr) {
final long startTime = System.nanoTime();
final Collection<Column> columns = columnsToFetch(conf, req);
LOG.i("Updating columns: %s.", Column.titles(columns));
final Collection<FetchFeedRequest> fetches = feedsToFetchs(conf, columns);
fetchFeeds(providerMgr, req, fetches);
if (!req.manual) Notifications.update(getBaseContext(), getDb(), columns);
final long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
LOG.i("Fetched %d columns in %d millis.", columns.size(), durationMillis);
return columns;
}
private Collection<Column> columnsToFetch (final Config conf, final UpdateRequest req) {
final Collection<Column> columns = new ArrayList<Column>();
if (req.columnIds != null && req.columnIds.length > 0) {
for (final int columnId : req.columnIds) {
final Column column = conf.getColumnById(columnId);
if (column != null) {
columns.add(column);
}
else {
LOG.i("Column not found: %s", columnId);
}
}
}
else {
columns.addAll(conf.getColumns());
}
if (!req.manual) removeNotDue(columns);
// For now treating the configured interval as an 'attempt rate' not 'success rate' so write update time now.
final long now = System.currentTimeMillis();
for (final Column column : columns) {
getDb().storeValue(KvKeys.colLastRefreshTime(column), String.valueOf(now));
}
return columns;
}
private void removeNotDue (final Collection<Column> columns) {
final Iterator<Column> colItr = columns.iterator();
final long now = System.currentTimeMillis();
while (colItr.hasNext()) {
final Column column = colItr.next();
final int refIntMins = column.getRefreshIntervalMins();
if (refIntMins < 1) colItr.remove(); // Do not refresh columns not configured to refresh.
final String lastTimeRaw = getDb().getValue(KvKeys.colLastRefreshTime(column));
if (lastTimeRaw == null) continue; // Never refreshed.
final long lastTime = Long.parseLong(lastTimeRaw);
if (lastTime <= 0L) continue; // Probably never refreshed.
if (now - lastTime < TimeUnit.MINUTES.toMillis(refIntMins)) colItr.remove(); // Do not refresh up to date columns.
}
}
private static Collection<FetchFeedRequest> feedsToFetchs (final Config conf, final Collection<Column> columns) {
final Collection<FetchFeedRequest> ret = new ArrayList<FetchFeedRequest>();
for (final Column col : columns) {
for (final ColumnFeed feed : col.getFeeds()) {
final Account account = resolveFeedAccount(conf, feed);
if (account != null) ret.add(new FetchFeedRequest(col, feed, account));
}
}
return ret;
}
private void fetchFeeds (final ProviderMgr providerMgr, final UpdateRequest req, final Collection<FetchFeedRequest> fetches) {
if (fetches.size() >= C.UPDATER_MIN_COLUMS_TO_USE_THREADPOOL) {
fetchFeedsMultiThread(providerMgr, fetches, req);
}
else {
fetchFeedsSingleThread(providerMgr, fetches, req);
}
}
private void fetchFeedsSingleThread (final ProviderMgr providerMgr, final Collection<FetchFeedRequest> feeds, final UpdateRequest req) {
for (final FetchFeedRequest feed : feeds) {
FetchColumn.fetchColumn(getDb(), feed, providerMgr, req.filters);
}
}
private void fetchFeedsMultiThread (final ProviderMgr providerMgr, final Collection<FetchFeedRequest> feeds, final UpdateRequest req) {
final int poolSize = Math.min(feeds.size(), C.UPDATER_MAX_THREADS);
LOG.i("Using thread pool size %d for %d feeds.", poolSize, feeds.size());
final ExecutorService ex = Executors.newFixedThreadPool(poolSize);
try {
final Map<FetchFeedRequest, Future<Void>> jobs = new LinkedHashMap<FetchFeedRequest, Future<Void>>();
for (final FetchFeedRequest feed : feeds) {
jobs.put(feed, ex.submit(new FetchColumn(getDb(), feed, providerMgr, req.filters)));
}
for (final Entry<FetchFeedRequest, Future<Void>> job : jobs.entrySet()) {
try {
job.getValue().get();
}
catch (final InterruptedException e) {
LOG.w("Error fetching feed '%s': %s %s", job.getKey().getUiTitle(), e.getClass().getName(), e.toString());
}
catch (final ExecutionException e) {
LOG.w("Error fetching feed '%s': %s %s", job.getKey().getUiTitle(), e.getClass().getName(), e.toString());
}
}
}
finally {
ex.shutdownNow();
}
}
private static Account resolveFeedAccount (final Config conf, final ColumnFeed feed) {
final Account account = conf.getAccount(feed.getAccountId());
if (account == null) LOG.e("Unknown acountId: '%s'.", feed.getAccountId());
return account;
}
private static class UpdateRequest {
public final int[] columnIds;
public final boolean manual;
public final Filters filters;
public UpdateRequest (final int[] columnIds, final boolean manual, final Filters filters) {
this.columnIds = columnIds;
this.manual = manual;
this.filters = filters;
}
}
}