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 map = new HashMap() { { 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... params) { JsonElement content = super.doInBackground(params); if (content != null) { try { Type listType = new TypeToken>() {}.getType(); List 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 map = new HashMap() { { 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... params) { JsonElement content = super.doInBackground(params); if (content != null) { try { Type listType = new TypeToken>() {}.getType(); List 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 map = new HashMap() { { 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
m_articles; public OfflineArticlesRequest(Context context) { super(context); } @Override protected JsonElement doInBackground(HashMap... params) { JsonElement content = super.doInBackground(params); if (content != null) { try { Type listType = new TypeToken>() {}.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(); } } }