package com.newsrob;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.ClientProtocolException;
import org.xml.sax.SAXException;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import com.newsrob.EntryManager.SyncJobStatus;
import com.newsrob.activities.UIHelper;
import com.newsrob.download.DownloadCancelledException;
import com.newsrob.download.DownloadException;
import com.newsrob.download.DownloadTimedOutException;
import com.newsrob.download.WebPageDownloadDirector;
import com.newsrob.jobs.Job;
import com.newsrob.jobs.ModelUpdateResult;
import com.newsrob.jobs.SynchronizeModelFailed;
import com.newsrob.jobs.SynchronizeModelSucceeded;
import com.newsrob.storage.IStorageAdapter;
import com.newsrob.threetosix.R;
import com.newsrob.util.PreviewGenerator;
import com.newsrob.util.SDK9Helper;
import com.newsrob.util.Timing;
import com.newsrob.util.U;
public class SynchronizationService extends Service {
private static final String TAG = SynchronizationService.class.getSimpleName();
static final String ACTION_SYNC_UPLOAD_ONLY = "upload_only";
public static final String EXTRA_MANUAL_SYNC = "manual_sync";
private static final String PREF_KEY_LAST_STARTED = "com.newsrob.synchronization.lastStarted";
private static WakeLock wl;
private Handler handler;
private static final Class[] mStartForegroundSignature = new Class[] { int.class, Notification.class };
private static final Class[] mStopForegroundSignature = new Class[] { boolean.class };
private NotificationManager mNM;
private Method mStartForeground;
private Method mStopForeground;
private Object[] mStartForegroundArgs = new Object[2];
private Object[] mStopForegroundArgs = new Object[1];
private EntryManager entryManager;
@Override
public void onCreate() {
super.onCreate();
mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
try {
mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
} catch (NoSuchMethodException e) {
// Running on an older platform.
mStartForeground = mStopForeground = null;
}
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onCreate() called.");
handler = new Handler();
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
if (intent == null) {
stopSelf();
getEntryManager().getNewsRobNotificationManager().cancelSyncInProgressNotification();
Log.d(TAG, "onStart() called with intent == null. Stopping self.");
}
if (getEntryManager().isModelCurrentlyUpdated())
return;
String lastStarted = getEntryManager().getSharedPreferences().getString(PREF_KEY_LAST_STARTED, "");
if (!"".equals(lastStarted)) {
String message = "The synchronization started at " + lastStarted + " was ended prematurely.";
if (NewsRob.isDebuggingEnabled(this))
PL.log(message, this);
else
Log.w(TAG, message);
}
setLastStarted(new Date().toString());
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onStart() called. startId=" + startId + " intent=" + intent);
boolean uO = false;
boolean mS = false;
try {
uO = ACTION_SYNC_UPLOAD_ONLY.equals(intent.getAction());
mS = intent.getBooleanExtra(EXTRA_MANUAL_SYNC, false);
} catch (NullPointerException npe) {
//
}
final boolean uploadOnly = uO;
final boolean manualSync = mS;
new Thread(new Runnable() {
public void run() {
if (wl == null)
// throw new
// RuntimeException("Oh, oh. No wake lock acquired!");
acquireWakeLock(getApplicationContext());
startNotifying(uploadOnly);
try {
doSync(uploadOnly, manualSync);
} finally {
stopNotifying();
handler.post(new Runnable() {
public void run() {
stopSelf();
}
});
}
}
}).start();
}
void startNotifying(boolean fastSyncOnly) {
/*
* if (false && getEntryManager().isSyncInProgressNotificationEnabled())
* startForegroundCompat(999,
* getEntryManager().getNewsRobNotificationManager()
* .createSynchronizationRunningNotificationUsingRemoteViews
* (fastSyncOnly));
*/
// setForeground(true);
}
void stopNotifying() {
if (false && getEntryManager().isSyncInProgressNotificationEnabled())
stopForegroundCompat(999);
// setForeground(false);
}
private void setLastStarted(String value) {
SDK9Helper.apply(getEntryManager().getSharedPreferences().edit().putString(PREF_KEY_LAST_STARTED, value));
}
private void resetLastStarted() {
setLastStarted("");
}
@Override
public void onLowMemory() {
super.onLowMemory();
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onLowMemory() called.");
}
@Override
public void onDestroy() {
super.onDestroy();
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onDestroy() called.");
}
@Override
public IBinder onBind(Intent intent) {
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onBind() called.");
return null;
}
@Override
public void onRebind(Intent intent) {
super.onRebind(intent);
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onRebind() called.");
}
@Override
public boolean onUnbind(Intent intent) {
if (NewsRob.isDebuggingEnabled(this))
Log.d(TAG, "onUnbind() called.");
return super.onUnbind(intent);
}
protected void doSync(final boolean uploadOnly, final boolean manualSync) {
final EntryManager entryManager = getEntryManager();
final EntriesRetriever grf = entryManager.getEntriesRetriever();
final IStorageAdapter fileContextAdapter = entryManager.getStorageAdapter();
if (entryManager.isModelCurrentlyUpdated())
return;
entryManager.lockModel("SSer.doSync");
PL.log("SynchronizationService. Used settings: "
+ SettingsRenderer.renderSettings(entryManager, new StringBuilder("\n")), SynchronizationService.this);
// entryManager.runningThread = new Thread(new Runnable() {
Throwable caughtThrowable = null;
// public void run() {
final Context ctx = getApplicationContext();
PL.log("SynchronizationService - start", SynchronizationService.this);
PL.log("Last successful login: " + entryManager.getLastSuccessfulLogin(), SynchronizationService.this);
// Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
Timing t = new Timing("Synchronization Runnable", this);
long started = System.currentTimeMillis();
Log.i(TAG, "Synchronization started at " + new Date().toString() + ". started=" + started);
final SyncJobStatus syncJobStatus = new SyncJobStatus();
// last used
long lastUsed = entryManager.getLastUsed();
ModelUpdateResult result = null;
try {
PL.log("Run Mark - in Try", SynchronizationService.this);
if (!uploadOnly) {
try {
if (!Feed.restoreFeedsIfNeccesary(this))
Feed.saveFeedSettings(this);
} catch (Exception e) {
e.printStackTrace();
}
}
entryManager.fireModelUpdateStarted("Synchronization", uploadOnly, manualSync);
List<Job> jobList = new LinkedList<Job>();
Job deleteReadArticlesJob = new DeleteArticlesJob(SynchronizationService.this, entryManager, syncJobStatus);
Job reduceToCapacityJob = new ReduceToCapacityJob(SynchronizationService.this, entryManager, syncJobStatus);
if (!uploadOnly) {
if (entryManager.shouldReadItemsBeDeleted())
jobList.add(deleteReadArticlesJob);
jobList.add(reduceToCapacityJob);
}
jobList.add(new Job("Submitting annotated articles", entryManager) {
@Override
public void run() throws Exception {
if (entryManager.syncCurrentlyEnabled(manualSync))
entryManager.getEntriesRetriever().submitNotes(this);
}
});
jobList.add(new SyncChangedArticlesStatusJob(SynchronizationService.this, entryManager, syncJobStatus,
manualSync));
if (!uploadOnly && entryManager.shouldReadItemsBeDeleted())
jobList.add(deleteReadArticlesJob);
Job removeDeletedNotes = new Job("Removing submitted notes", entryManager) {
@Override
public void run() throws Exception {
if (entryManager.syncCurrentlyEnabled(manualSync))
entryManager.getEntriesRetriever().removeDeletedNotes();
}
};
if (!uploadOnly) {
jobList.add(new Job("Unsubscribing from feeds", entryManager) {
@Override
public void run() throws Exception {
if (!entryManager.syncCurrentlyEnabled(manualSync))
return;
Cursor c = entryManager.getFeeds2UnsubscribeCursor();
try {
while (c.moveToNext()) {
String feedAtomId = c.getString(1);
PL.log("Unsubscribing: " + feedAtomId, SynchronizationService.this);
entryManager.getEntriesRetriever().unsubscribeFeed(feedAtomId);
}
} finally {
c.close();
}
}
});
}
if (!uploadOnly)
jobList.add(removeDeletedNotes);
if (!uploadOnly)
jobList.add(new FetchUnreadArticlesJob(SynchronizationService.this, entryManager, syncJobStatus,
manualSync));
if (!uploadOnly)
jobList.add(new Job("Daily update of subscriptions (feed titles)", entryManager) {
@Override
public void run() throws IOException, ParserConfigurationException, SAXException,
GRTokenExpiredException {
if (entryManager.syncCurrentlyEnabled(manualSync)) {
grf.updateSubscriptionList(entryManager, this);
entryManager.fireModelUpdated();
}
}
});
if (!uploadOnly)
jobList.add(reduceToCapacityJob);
if (!uploadOnly && entryManager.shouldReadItemsBeDeleted())
jobList.add(deleteReadArticlesJob);
if (!uploadOnly) {
// make sure that a manual sync moves the automatic sync
// forward,
// i.e. when pushing "Refresh" in the middle of a 24h sync,
// reset timer to 0, so that it will be another 24h from this
// point on
entryManager.getScheduler().resetBackgroundSchedule();
final float screenSizeFactor = getScreenSizeFactor(ctx);
final float previewScaleFactor = ctx.getResources().getDisplayMetrics().density;
jobList.add(new Job("Downloading articles", entryManager) {
private int currentArticle = 0;
private Collection<Long> entries2Download;
@Override
public void run() {
if (entryManager.getSharedPreferences().getString(EntryManager.SETTINGS_STORAGE_ASSET_DOWNLOAD,
EntryManager.DOWNLOAD_YES).equals(EntryManager.DOWNLOAD_NO)) {
Log
.d(TAG,
"Downloading of assets is disabled in the settings. Therefore skipping downloading webpages.");
return;
}
Timing tSql = new Timing("SQL Query findAllToDownload", SynchronizationService.this);
entries2Download = entryManager.findAllArticleIds2Download();
tSql.stop();
Timing tOutter = new Timing("Downloading all " + entries2Download.size()
+ " pages or well, the ones that were downloaded", SynchronizationService.this);
for (Long articleId : entries2Download) {
// get the current data
// LATER use a real cursor and somehow find out when
// data became stale
Entry entry = entryManager.findArticleById(articleId);
if (entry == null)
continue;
if (!entryManager.downloadContentCurrentlyEnabled(manualSync))
return;
if (!fileContextAdapter.canWrite()) {
Log
.d(
TAG,
"File context adapter ("
+ fileContextAdapter.getClass().getName()
+ ") cannot be written to at the moment. Mounted? Read Only? Not downloading web pages.");
return;
}
if (isCancelled())
break;
// System.out.println("----------------- " +
// entry.getTitle());
currentArticle++;
entryManager.fireStatusUpdated();
// don't download read entries, except the starred
// ones
if (entry.getReadState() == ReadState.READ && !entry.isStarred() && !entry.isNote())
continue;
int resolvedDownloadPref = entry.getResolvedDownloadPref(entryManager);
if (resolvedDownloadPref == Feed.DOWNLOAD_HEADERS_ONLY) {
// entry.setDownloaded(Entry.STATE_DOWNLOADED_FULL_PAGE
// : Entry.STATE_DOWNLOADED_FEED_CONTENT);
// entry.setError(null);
// entryManager.fireModelUpdated();
continue;
}
// check against the db, because in the
// meantime
// the
// read status might have changed
Timing tInner = new Timing("Downloading page " + entry.getAlternateHRef(),
SynchronizationService.this);
try {
// check free space
float freeSpaceLeft = fileContextAdapter.megaBytesFree();
Log
.d(TAG, String.format("Free space remaining for downloads: %.2f MB.",
freeSpaceLeft));
if (freeSpaceLeft < 0) {
PL.log(TAG + ": Oh no, free space left is a negative value ;-( Ignoring it.", ctx);
} else if (freeSpaceLeft < fileContextAdapter.megaBytesThreshold()) {
PL.log(TAG + ": Not enough space left to download page.", ctx);
entryManager.getNewsRobNotificationManager()
.createSyncSpaceExceededProblemNotification(
fileContextAdapter.megaBytesThreshold());
break;
}
boolean downloadTheWholePage = (resolvedDownloadPref == Feed.DOWNLOAD_PREF_FEED_AND_MOBILE_WEBPAGE || resolvedDownloadPref == Feed.DOWNLOAD_PREF_FEED_AND_WEBPAGE);
String summary = entry.getContent() != null ? entry.getContent() : UIHelper.linkize(
entry.getAlternateHRef(), entry.getTitle());
WebPageDownloadDirector.downloadWebPage(entry.getShortAtomId(), new URL(entry
.getBaseUrl(entryManager)), fileContextAdapter, this, summary,
downloadTheWholePage, entryManager, manualSync);
if (true) {
File assetsDir = entry.getAssetsDir(entryManager);
System.out.println("generatePreview="
+ new PreviewGenerator(ctx, assetsDir,
(int) (100 * previewScaleFactor * screenSizeFactor),
(int) (100 * previewScaleFactor * screenSizeFactor),
(int) (6 * previewScaleFactor)).generatePreview());
}
// TODO
// only
// one
// instance
// TODO use display metrics?
// TODO use orientation? Larger thumbs for
// larger screens?
entry.setDownloaded(downloadTheWholePage ? Entry.STATE_DOWNLOADED_FULL_PAGE
: Entry.STATE_DOWNLOADED_FEED_CONTENT);
entry.setError(null);
entryManager.fireModelUpdated();
} catch (Exception e) {
Log.e(TAG, "Problem dowloading page " + entry.getAlternateHRef() + ".", e);
Throwable cause = null;
if (e instanceof DownloadException) {
cause = ((DownloadException) e).getCause();
Log.d(TAG, "DownloadException cause=" + cause);
} else
Log.d(TAG, "Exception=" + e);
boolean downloadError = false;
if (e instanceof DownloadCancelledException
|| cause != null
&& (cause instanceof FileNotFoundException
|| cause instanceof SocketTimeoutException
|| cause instanceof SocketException
|| cause instanceof NoHttpResponseException
|| cause instanceof UnknownHostException
|| cause instanceof DownloadCancelledException || cause instanceof DownloadTimedOutException)) {
Log.d(TAG, "Caught a FNFE");
} else {
Log.d(TAG, "Marked download as error.");
downloadError = true;
}
StringBuilder renderedStackTrace = new StringBuilder();
U.renderStackTrace(e, renderedStackTrace);
entry.setError(cause != null ? "Cause: " + cause.getClass().getSimpleName() + ": "
+ cause.getMessage() : e.getClass().getSimpleName() + ": " + e.getMessage()
+ "\nStacktrace: " + renderedStackTrace);
entry.setDownloaded(downloadError ? Entry.STATE_DOWNLOAD_ERROR
: Entry.STATE_NOT_DOWNLOADED);
}
entryManager.updatedDownloaded(entry);
tInner.stop();
}
tOutter.stop();
}
@Override
public boolean isProgressMeassurable() {
return true;
}
@Override
public int[] getProgress() {
return new int[] { currentArticle, entries2Download != null ? entries2Download.size() : 0 };
}
});
}
PL.log("Run Mark - Jobs added", this);
entryManager.runJobs(jobList);
Log.d(TAG, "NoOfEntriesUpdated=" + syncJobStatus.noOfEntriesUpdated);
Log.d(TAG, "NoOfEntriesFetched=" + syncJobStatus.noOfEntriesFetched);
PL.log("Run Mark - Mission accomplished. -> complete ", this);
result = new SynchronizeModelSucceeded(syncJobStatus.noOfEntriesUpdated);
} catch (Throwable throwable) {
result = new SynchronizeModelFailed(throwable);
Log.d(TAG, "Problem during synchronization.", throwable);
} finally {
PL.log("Run Mark - In Finally", this);
entryManager.unlockModel("SSer.doSync");
entryManager.clearCancelState();
entryManager.fireModelUpdateFinished(result);
entryManager.fireStatusUpdated();
Log.i(TAG, "Synchronization finished at " + new Date().toString() + ". started=" + started);
t.stop();
if (!uploadOnly)
entryManager.setLastSync(caughtThrowable == null);
int noOfNewArticles = entryManager.getNoOfNewArticlesSinceLastUsed(lastUsed);
entryManager.getNewsRobNotificationManager().notifyNewArticles(entryManager, lastUsed, noOfNewArticles);
PL.log("Run Mark - End of Finally", this);
resetLastStarted();
releaseWakeLock();
}
}
private float getScreenSizeFactor(final Context ctx) {
int size = U.getScreenSize(ctx);
if (size > 0)
return 1.25f;
else if (size < 0)
return 0.75f;
return 1.0f;
}
private EntryManager getEntryManager() {
if (entryManager == null)
entryManager = EntryManager.getInstance(getApplicationContext());
return entryManager;
}
public static void acquireWakeLock(Context ctx) {
if (wl != null)
return;
PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wl.acquire();
}
public static void releaseWakeLock() {
try {
if (wl != null)
wl.release();
wl = null;
} catch (Exception e) {
Log.e(TAG, "Oops. Problem when releasing wake lock.", e);
}
}
/**
* This is a wrapper around the new startForeground method, using the older
* APIs if it is not available.
*/
void startForegroundCompat(int id, Notification notification) {
// If we have the new startForeground API, then use it.
if (mStartForeground != null) {
mStartForegroundArgs[0] = Integer.valueOf(id);
mStartForegroundArgs[1] = notification;
try {
mStartForeground.invoke(this, mStartForegroundArgs);
} catch (InvocationTargetException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke startForeground", e);
} catch (IllegalAccessException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke startForeground", e);
}
return;
}
mNM.notify(id, notification);
}
/**
* This is a wrapper around the new stopForeground method, using the older
* APIs if it is not available.
*/
void stopForegroundCompat(int id) {
// If we have the new stopForeground API, then use it.
if (mStopForeground != null) {
mStopForegroundArgs[0] = Boolean.TRUE;
try {
mStopForeground.invoke(this, mStopForegroundArgs);
} catch (InvocationTargetException e) {
// Should not happen.
Log.w(TAG, "Unable to invoke stopForeground", e);
} catch (IllegalAccessException e) {
// Should not happen.
Log.w(TAG, "Unable to invoke stopForeground", e);
}
return;
}
// Fall back on the old API. Note to cancel BEFORE changing the
// foreground state, since we could be killed at that point.
mNM.cancel(id);
setForeground(false);
}
}
abstract class SyncJob extends Job {
private EntryManager entryManager;
private Context context;
private SyncJobStatus status;
public int target;
public int actual;
SyncJob(Context context, EntryManager entryManager, SyncJobStatus status, String message) {
super(message, entryManager);
this.entryManager = entryManager;
this.context = context;
this.status = status;
}
protected EntryManager getEntryManager() {
return entryManager;
}
protected Context getContext() {
return context;
}
protected SyncJobStatus getSyncJobStatus() {
return status;
}
@Override
public boolean isProgressMeassurable() {
return target != -1;
}
@Override
public int[] getProgress() {
return new int[] { actual, target };
}
protected abstract int doRun() throws Throwable;
public void run() throws Throwable {
PL.log("About to be executed: " + getJobDescription(), context);
target = -1;
actual = -1;
int noOfArticlesAffected = doRun();
PL.log("No of articles affected=" + noOfArticlesAffected, context);
status.noOfEntriesUpdated += noOfArticlesAffected;
if (status.noOfEntriesUpdated > 0)
entryManager.fireModelUpdated();
}
}
class DeleteArticlesJob extends SyncJob {
public DeleteArticlesJob(Context context, EntryManager entryManager, SyncJobStatus status) {
super(context, entryManager, status, "Deleting read articles");
}
@Override
public int doRun() {
return getEntryManager().deleteReadArticles(this);
}
}
class ReduceToCapacityJob extends SyncJob {
public ReduceToCapacityJob(Context context, EntryManager entryManager, SyncJobStatus status) {
super(context, entryManager, status, "Deleting oldest articles over capacity");
}
@Override
public int doRun() {
return getEntryManager().reduceToCapacity(this);
}
}
class FetchUnreadArticlesJob extends SyncJob {
private boolean manualSync;
public FetchUnreadArticlesJob(Context context, EntryManager entryManager, SyncJobStatus status, boolean manualSync) {
super(context, entryManager, status, "Fetching new articles from Google Reader");
this.manualSync = manualSync;
}
@Override
public int doRun() throws ClientProtocolException, IllegalStateException, IOException, NeedsSessionException,
SAXException, ParserConfigurationException, FactoryConfigurationError, ReaderAPIException,
GRTokenExpiredException {
if (!getEntryManager().syncCurrentlyEnabled(manualSync))
return 0;
final EntriesRetriever grf = getEntryManager().getEntriesRetriever();
int noOfEntriesFetched = 0;
noOfEntriesFetched = grf.fetchNewEntries(getEntryManager(), this, manualSync);
getSyncJobStatus().noOfEntriesFetched = noOfEntriesFetched;
getEntryManager().fireModelUpdated();
return noOfEntriesFetched;
}
}
class SyncChangedArticlesStatusJob extends SyncJob {
private boolean manualSync;
SyncChangedArticlesStatusJob(Context context, EntryManager entryManager, SyncJobStatus status, boolean manualSync) {
super(context, entryManager, status, "Sync status of changed articles");
this.manualSync = manualSync;
}
@Override
public int doRun() throws MalformedURLException, IOException, ParserConfigurationException,
FactoryConfigurationError, SAXException, ParseException, NeedsSessionException {
if (!getEntryManager().syncCurrentlyEnabled(manualSync))
return 0;
int noOfEntriesUpdated = getEntryManager().getEntriesRetriever().synchronizeWithGoogleReader(getEntryManager(),
this);
getSyncJobStatus().noOfEntriesUpdated += noOfEntriesUpdated;
if (noOfEntriesUpdated > 0)
getEntryManager().fireModelUpdated();
return noOfEntriesUpdated;
}
}