tt-rss-android/org.fox.ttrss/src/main/java/org/fox/ttrss/offline/OfflineDownloadService.java

501 lines
14 KiB
Java

package org.fox.ttrss.offline;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import org.fox.ttrss.ApiRequest;
import org.fox.ttrss.OnlineActivity;
import org.fox.ttrss.R;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.types.FeedCategory;
import org.fox.ttrss.util.DatabaseHelper;
import org.fox.ttrss.util.ImageCacheService;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
public class OfflineDownloadService extends Service {
private final String TAG = this.getClass().getSimpleName();
public static final int NOTIFY_DOWNLOADING = 1;
public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.DownloadComplete";
public static final String INTENT_ACTION_CANCEL = "org.fox.ttrss.intent.action.Cancel";
private static final int OFFLINE_SYNC_SEQ = 50;
private static final int OFFLINE_SYNC_MAX = OFFLINE_SYNC_SEQ * 10;
private SQLiteDatabase m_writableDb;
private SQLiteDatabase m_readableDb;
private int m_articleOffset = 0;
private String m_sessionId;
private NotificationManager m_nmgr;
private boolean m_batchMode = false;
private boolean m_downloadInProgress = false;
private boolean m_downloadImages = false;
private int m_syncMax;
private SharedPreferences m_prefs;
private boolean m_canProceed = true;
private final IBinder m_binder = new LocalBinder();
public class LocalBinder extends Binder {
OfflineDownloadService getService() {
return OfflineDownloadService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return m_binder;
}
@Override
public void onCreate() {
super.onCreate();
m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
m_downloadImages = m_prefs.getBoolean("offline_image_cache_enabled", false);
m_syncMax = Integer.parseInt(m_prefs.getString("offline_sync_max", String.valueOf(OFFLINE_SYNC_MAX)));
initDatabase();
}
@SuppressWarnings("deprecation")
private void updateNotification(String msg) {
Notification notification = new Notification(R.drawable.icon,
getString(R.string.notify_downloading_title), System.currentTimeMillis());
Intent intent = new Intent(this, OnlineActivity.class);
intent.setAction(INTENT_ACTION_CANCEL);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
intent, 0);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
notification.setLatestEventInfo(this, getString(R.string.notify_downloading_title), msg, contentIntent);
m_nmgr.notify(NOTIFY_DOWNLOADING, notification);
}
private void updateNotification(int msgResId) {
updateNotification(getString(msgResId));
}
private void downloadFailed() {
m_readableDb.close();
m_writableDb.close();
m_nmgr.cancel(NOTIFY_DOWNLOADING);
// TODO send notification to activity?
m_downloadInProgress = false;
stopSelf();
}
private boolean isCacheServiceRunning() {
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if ("org.fox.ttrss.util.ImageCacheService".equals(service.service.getClassName())) {
return true;
}
}
return false;
}
public void downloadComplete() {
m_downloadInProgress = false;
// if cache service is running, it will send a finished intent on its own
if (!isCacheServiceRunning()) {
m_nmgr.cancel(NOTIFY_DOWNLOADING);
if (m_batchMode) {
SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = localPrefs.edit();
editor.putBoolean("offline_mode_active", true);
editor.commit();
} else {
Intent intent = new Intent();
intent.setAction(INTENT_ACTION_SUCCESS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
sendBroadcast(intent);
}
} else {
updateNotification(getString(R.string.notify_downloading_images, 0));
}
m_readableDb.close();
m_writableDb.close();
stopSelf();
}
private void initDatabase() {
DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
m_writableDb = dh.getWritableDatabase();
m_readableDb = dh.getReadableDatabase();
}
/* private synchronized SQLiteDatabase getReadableDb() {
return m_readableDb;
} */
private synchronized SQLiteDatabase getWritableDb() {
return m_writableDb;
}
@SuppressWarnings("unchecked")
private void downloadArticles() {
Log.d(TAG, "offline: downloading articles... offset=" + m_articleOffset);
updateNotification(getString(R.string.notify_downloading_articles, m_articleOffset));
OfflineArticlesRequest req = new OfflineArticlesRequest(this);
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getHeadlines");
put("sid", m_sessionId);
put("feed_id", "-4");
put("view_mode", "unread");
put("show_content", "true");
put("skip", String.valueOf(m_articleOffset));
put("limit", String.valueOf(OFFLINE_SYNC_SEQ));
}
};
req.execute(map);
}
private void downloadFeeds() {
updateNotification(R.string.notify_downloading_feeds);
getWritableDb().execSQL("DELETE FROM feeds;");
ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override
protected JsonElement doInBackground(HashMap<String, String>... params) {
JsonElement content = super.doInBackground(params);
if (content != null) {
try {
Type listType = new TypeToken<List<Feed>>() {}.getType();
List<Feed> feeds = new Gson().fromJson(content, listType);
SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO feeds " +
"("+BaseColumns._ID+", title, feed_url, has_icon, cat_id) " +
"VALUES (?, ?, ?, ?, ?);");
for (Feed feed : feeds) {
stmtInsert.bindLong(1, feed.id);
stmtInsert.bindString(2, feed.title);
stmtInsert.bindString(3, feed.feed_url);
stmtInsert.bindLong(4, feed.has_icon ? 1 : 0);
stmtInsert.bindLong(5, feed.cat_id);
stmtInsert.execute();
}
stmtInsert.close();
Log.d(TAG, "offline: done downloading feeds");
m_articleOffset = 0;
getWritableDb().execSQL("DELETE FROM articles;");
} catch (Exception e) {
e.printStackTrace();
updateNotification(R.string.offline_switch_error);
downloadFailed();
}
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed) {
downloadArticles();
} else {
downloadFailed();
}
} else {
updateNotification(getErrorMessage());
downloadFailed();
}
}
};
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getFeeds");
put("sid", m_sessionId);
put("cat_id", "-3");
put("unread_only", "true");
}
};
req.execute(map);
}
private void downloadCategories() {
updateNotification(R.string.notify_downloading_feeds);
getWritableDb().execSQL("DELETE FROM categories;");
ApiRequest req = new ApiRequest(getApplicationContext()) {
protected JsonElement doInBackground(HashMap<String, String>... params) {
JsonElement content = super.doInBackground(params);
if (content != null) {
try {
Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
List<FeedCategory> cats = new Gson().fromJson(content, listType);
SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO categories " +
"("+BaseColumns._ID+", title) " +
"VALUES (?, ?);");
for (FeedCategory cat : cats) {
stmtInsert.bindLong(1, cat.id);
stmtInsert.bindString(2, cat.title);
stmtInsert.execute();
}
stmtInsert.close();
Log.d(TAG, "offline: done downloading categories");
} catch (Exception e) {
e.printStackTrace();
updateNotification(R.string.offline_switch_error);
downloadFailed();
}
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed) {
downloadFeeds();
} else {
downloadFailed();
}
} else {
updateNotification(getErrorMessage());
downloadFailed();
}
}
};
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getCategories");
put("sid", m_sessionId);
//put("cat_id", "-3");
put("unread_only", "true");
}
};
req.execute(map);
}
@Override
public void onDestroy() {
super.onDestroy();
m_nmgr.cancel(NOTIFY_DOWNLOADING);
m_canProceed = false;
Log.d(TAG, "onDestroy");
//m_readableDb.close();
//m_writableDb.close();
}
public class OfflineArticlesRequest extends ApiRequest {
List<Article> m_articles;
public OfflineArticlesRequest(Context context) {
super(context);
}
@Override
protected JsonElement doInBackground(HashMap<String, String>... params) {
JsonElement content = super.doInBackground(params);
if (content != null) {
try {
Type listType = new TypeToken<List<Article>>() {}.getType();
m_articles = new Gson().fromJson(content, listType);
SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " +
"("+BaseColumns._ID+", unread, marked, published, score, updated, is_updated, title, link, feed_id, tags, content, author) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
for (Article article : m_articles) {
String tagsString = "";
for (String t : article.tags) {
tagsString += t + ", ";
}
tagsString = tagsString.replaceAll(", $", "");
int index = 1;
stmtInsert.bindLong(index++, article.id);
stmtInsert.bindLong(index++, article.unread ? 1 : 0);
stmtInsert.bindLong(index++, article.marked ? 1 : 0);
stmtInsert.bindLong(index++, article.published ? 1 : 0);
stmtInsert.bindLong(index++, article.score);
stmtInsert.bindLong(index++, article.updated);
stmtInsert.bindLong(index++, article.is_updated ? 1 : 0);
stmtInsert.bindString(index++, article.title);
stmtInsert.bindString(index++, article.link);
stmtInsert.bindLong(index++, article.feed_id);
stmtInsert.bindString(index++, tagsString); // comma-separated tags
stmtInsert.bindString(index++, article.content);
stmtInsert.bindString(index++, article.author != null ? article.author : "");
if (m_downloadImages) {
Document doc = Jsoup.parse(article.content);
if (doc != null) {
Elements images = doc.select("img");
for (Element img : images) {
String url = img.attr("src");
if (url.indexOf("://") != -1) {
if (!ImageCacheService.isUrlCached(OfflineDownloadService.this, url)) {
Intent intent = new Intent(OfflineDownloadService.this,
ImageCacheService.class);
intent.putExtra("url", url);
startService(intent);
}
}
}
}
}
try {
stmtInsert.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
m_articleOffset += m_articles.size();
Log.d(TAG, "offline: received " + m_articles.size() + " articles; canProc=" + m_canProceed);
stmtInsert.close();
} catch (Exception e) {
updateNotification(R.string.offline_switch_error);
Log.d(TAG, "offline: failed: exception when loading articles");
e.printStackTrace();
downloadFailed();
}
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed && m_articles != null) {
if (m_articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) {
downloadArticles();
} else {
downloadComplete();
}
} else {
downloadFailed();
}
} else {
Log.d(TAG, "offline: failed: " + getErrorMessage());
updateNotification(getErrorMessage());
downloadFailed();
}
}
}
@Override
public void onStart(Intent intent, int startId) {
try {
if (getWritableDb().isDbLockedByCurrentThread() || getWritableDb().isDbLockedByOtherThreads()) {
return;
}
m_sessionId = intent.getStringExtra("sessionId");
m_batchMode = intent.getBooleanExtra("batchMode", false);
if (!m_downloadInProgress) {
if (m_downloadImages) ImageCacheService.cleanupCache(this, false);
updateNotification(R.string.notify_downloading_init);
m_downloadInProgress = true;
downloadCategories();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}