/* * Copyright (c) 2015, Nils Braden * * This file is part of ttrss-reader-fork. This program is free software; you * can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; * either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. You should have received a * copy of the GNU General Public License along with this program; If * not, see http://www.gnu.org/licenses/. */ package org.ttrssreader.controllers; import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManager; import android.util.Log; import org.ttrssreader.R; import org.ttrssreader.imageCache.ImageCache; import org.ttrssreader.model.pojos.Article; import org.ttrssreader.model.pojos.Category; import org.ttrssreader.model.pojos.Feed; import org.ttrssreader.model.pojos.Label; import org.ttrssreader.net.IArticleOmitter; import org.ttrssreader.net.IdUnreadArticleOmitter; import org.ttrssreader.net.IdUpdatedArticleOmitter; import org.ttrssreader.net.JSONConnector; import org.ttrssreader.utils.Utils; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @SuppressLint("UseSparseArrays") public class Data { private static final String TAG = Data.class.getSimpleName(); private static final int VCAT_UNCAT = 0; public static final int VCAT_STAR = -1; public static final int VCAT_PUB = -2; public static final int VCAT_FRESH = -3; public static final int VCAT_ALL = -4; private static final String VIEW_ALL = "all_articles"; private static final String VIEW_UNREAD = "unread"; private long time; private long articlesCached; private Map<Integer, Long> articlesChanged; /** * map of category id to last changed time */ private Map<Integer, Long> feedsChanged; private long virtCategoriesChanged; private long categoriesChanged; private ConnectivityManager cm; // Singleton (see http://stackoverflow.com/a/11165926) private Data() { initTimers(); } public void initTimers() { time = 0; articlesCached = 0; articlesChanged = new HashMap<>(); feedsChanged = new HashMap<>(); virtCategoriesChanged = 0; categoriesChanged = 0; } private static class InstanceHolder { private static final Data instance = new Data(); } public static Data getInstance() { return InstanceHolder.instance; } public synchronized void initialize(final Context context) { if (context != null) cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); } // *** ARTICLES ********************************************************************* /** * cache all articles * * @param overrideOffline do not check connected state * @param overrideDelay if set to {@code true} enforces the update, otherwise the time from last update will be * considered */ public void cacheArticles(boolean overrideOffline, boolean overrideDelay) { int limit = 400; if (Controller.getInstance().isLowMemory()) limit = limit / 2; if (!overrideDelay && (time > (System.currentTimeMillis() - Utils.UPDATE_TIME))) { return; } else if (!Utils.isConnected(cm) && !(overrideOffline && Utils.checkConnected(cm))) { return; } Set<Article> articles = new HashSet<>(); int sinceId = Controller.getInstance().getSinceId(); long timeStart = System.currentTimeMillis(); IArticleOmitter filter = new IdUpdatedArticleOmitter("isUnread>0", 0); Controller.getInstance().getConnector() .getHeadlines(articles, VCAT_ALL, limit, VIEW_UNREAD, true, 0, null, filter); final Article newestCachedArticle = DBHelper.getInstance().getArticle(sinceId); IArticleOmitter updatedFilter = null; if (newestCachedArticle != null) updatedFilter = new IdUnreadArticleOmitter(newestCachedArticle.updated); Controller.getInstance().getConnector() .getHeadlines(articles, VCAT_ALL, limit, VIEW_ALL, true, sinceId, null, updatedFilter); handleInsertArticles(articles, true); time = System.currentTimeMillis(); notifyListeners(); // Store all category-ids and ids of all feeds for this category in db articlesCached = time; for (Category c : DBHelper.getInstance().getAllCategories()) { feedsChanged.put(c.id, time); } if (!articles.isEmpty() || !filter.getOmittedArticles().isEmpty()) { Set<Integer> articleUnreadIds = new HashSet<>(); articleUnreadIds.addAll(filter.getOmittedArticles()); for (Article a : articles) { if (a.isUnread) articleUnreadIds.add(a.id); } Log.d(TAG, "Amount of unread articles: " + articleUnreadIds.size()); DBHelper.getInstance().markRead(VCAT_ALL, false); DBHelper.getInstance().markArticles(articleUnreadIds, "isUnread", 1); } Log.d(TAG, "cacheArticles() Took: " + (System.currentTimeMillis() - timeStart) + "ms"); } /** * update articles for specified feed/category * * @param feedId feed/category to be updated * @param displayOnlyUnread flag, that indicates, that only unread articles should be shown * @param isCat if set to {@code true}, then {@code feedId} is actually the category ID * @param overrideOffline should the "work offline" state be ignored? * @param overrideDelay should the last update time be ignored? */ public void updateArticles(int feedId, boolean displayOnlyUnread, boolean isCat, boolean overrideOffline, boolean overrideDelay) { Long time = articlesChanged.get(feedId); if (isCat) // Category-Ids are in feedsChanged time = feedsChanged.get(feedId); if (time == null) time = 0L; if (articlesCached > time && !(feedId == VCAT_PUB || feedId == VCAT_STAR)) time = articlesCached; if (!overrideDelay && time > System.currentTimeMillis() - Utils.UPDATE_TIME) { return; } else if (!Utils.isConnected(cm) && !(overrideOffline && Utils.checkConnected(cm))) { return; } boolean isVcat = (feedId == VCAT_PUB || feedId == VCAT_STAR); int sinceId = 0; long timeStart = System.currentTimeMillis(); IArticleOmitter filter; if (isVcat) { displayOnlyUnread = false; filter = new IdUpdatedArticleOmitter("(isPublished>0 OR isStarred>0)"); } else { sinceId = Controller.getInstance().getSinceId(); filter = new IdUpdatedArticleOmitter(sinceId); } // Calculate an appropriate upper limit for the number of articles int limit = calculateLimit(feedId, isCat); if (Controller.getInstance().isLowMemory()) limit = limit / 2; Log.d(TAG, "UPDATE limit: " + limit); Set<Article> articles = new HashSet<>(); if (!displayOnlyUnread) { // If not displaying only unread articles: Refresh unread articles to get them too. Controller.getInstance().getConnector() .getHeadlines(articles, feedId, limit, VIEW_UNREAD, isCat, 0, null, null); } String viewMode = (displayOnlyUnread ? VIEW_UNREAD : VIEW_ALL); Controller.getInstance().getConnector() .getHeadlines(articles, feedId, limit, viewMode, isCat, sinceId, null, filter); if (isVcat) handlePurgeMarked(articles, feedId); handleInsertArticles(articles, false); long currentTime = System.currentTimeMillis(); // Store requested feed-/category-id and ids of all feeds in db for this category if a category was requested articlesChanged.put(feedId, currentTime); notifyListeners(); if (isCat) { for (Feed f : DBHelper.getInstance().getFeeds(feedId)) { articlesChanged.put(f.id, currentTime); } } Log.d(TAG, "updateArticles() Took: " + (System.currentTimeMillis() - timeStart) + "ms"); } /** * Calculate an appropriate upper limit for the number of articles */ private int calculateLimit(int feedId, boolean isCat) { int limit; switch (feedId) { case VCAT_STAR: // Starred case VCAT_PUB: // Published limit = JSONConnector.PARAM_LIMIT_MAX_VALUE; break; case VCAT_FRESH: // Fresh limit = DBHelper.getInstance().getUnreadCount(feedId, true); break; case VCAT_ALL: // All Articles limit = DBHelper.getInstance().getUnreadCount(feedId, true); break; default: // Normal categories limit = DBHelper.getInstance().getUnreadCount(feedId, isCat); } if (feedId < -10 && limit <= 0) // Unread-count in DB is wrong for Labels since we only count articles with // feedid = ? limit = 50; return limit; } private void handlePurgeMarked(Set<Article> articles, int feedId) { // TODO Mark all articles with ID > minId as "not starred" and "not published". But why? // Search min and max ids int minId = Integer.MAX_VALUE; Set<String> idSet = new HashSet<>(); for (Article article : articles) { if (article.id < minId) minId = article.id; idSet.add(article.id + ""); } String idList = Utils.separateItems(idSet, ","); String vcat; if (feedId == VCAT_STAR) vcat = "isStarred"; else if (feedId == VCAT_PUB) vcat = "isPublished"; else return; DBHelper.getInstance().handlePurgeMarked(idList, minId, vcat); } /** * prepare the DB and store given articles * * @param articles articles to be stored */ private void handleInsertArticles(final Collection<Article> articles, boolean isCaching) { if (!articles.isEmpty()) { // Search min and max ids int minId = Integer.MAX_VALUE; int maxId = Integer.MIN_VALUE; for (Article article : articles) { if (article.id > maxId) maxId = article.id; if (article.id < minId) minId = article.id; } DBHelper.getInstance().purgeLastArticles(articles.size()); DBHelper.getInstance().insertArticles(articles); // Only store sinceId when doing a full cache of new articles, else it doesn't work. if (isCaching) { Controller.getInstance().setSinceId(maxId); Controller.getInstance().setLastSync(System.currentTimeMillis()); } } } // *** FEEDS ************************************************************************ /** * update DB (delete/insert) with actual feeds information from server * * @param categoryId id of category, which feeds should be returned * @param overrideOffline do not check connected state * @return actual feeds for given category */ public Set<Feed> updateFeeds(int categoryId, boolean overrideOffline) { Long time = feedsChanged.get(categoryId); if (time == null) time = 0L; if (time > System.currentTimeMillis() - Utils.UPDATE_TIME) { return null; } else if (Utils.isConnected(cm) || (overrideOffline && Utils.checkConnected(cm))) { Set<Feed> ret = new LinkedHashSet<>(); Set<Feed> feeds = Controller.getInstance().getConnector().getFeeds(); // Only delete feeds if we got new feeds... if (!feeds.isEmpty()) { for (Feed f : feeds) { if (categoryId == VCAT_ALL || f.categoryId == categoryId) ret.add(f); feedsChanged.put(f.categoryId, System.currentTimeMillis()); } DBHelper.getInstance().deleteFeeds(); DBHelper.getInstance().insertFeeds(feeds); // Store requested category-id and ids of all received feeds feedsChanged.put(categoryId, System.currentTimeMillis()); notifyListeners(); } return ret; } return null; } // *** CATEGORIES ******************************************************************* public Set<Category> updateVirtualCategories(final Context context) { if (virtCategoriesChanged > System.currentTimeMillis() - Utils.UPDATE_TIME) return null; String vCatAll; String vCatFresh; String vCatPublished; String vCatStarred; String uncatFeeds; vCatAll = (String) context.getText(R.string.VCategory_AllArticles); vCatFresh = (String) context.getText(R.string.VCategory_FreshArticles); vCatPublished = (String) context.getText(R.string.VCategory_PublishedArticles); vCatStarred = (String) context.getText(R.string.VCategory_StarredArticles); uncatFeeds = (String) context.getText(R.string.Feed_UncategorizedFeeds); Set<Category> vCats = new LinkedHashSet<>(); vCats.add(new Category(VCAT_ALL, vCatAll, DBHelper.getInstance().getUnreadCount(VCAT_ALL, true))); vCats.add(new Category(VCAT_FRESH, vCatFresh, DBHelper.getInstance().getUnreadCount(VCAT_FRESH, true))); vCats.add(new Category(VCAT_PUB, vCatPublished, DBHelper.getInstance().getUnreadCount(VCAT_PUB, true))); vCats.add(new Category(VCAT_STAR, vCatStarred, DBHelper.getInstance().getUnreadCount(VCAT_STAR, true))); vCats.add(new Category(VCAT_UNCAT, uncatFeeds, DBHelper.getInstance().getUnreadCount(VCAT_UNCAT, true))); DBHelper.getInstance().insertCategories(vCats); notifyListeners(); virtCategoriesChanged = System.currentTimeMillis(); return vCats; } /** * update DB (delete/insert) with actual categories information from server * * @param overrideOffline do not check connected state * @return actual categories */ public Set<Category> updateCategories(boolean overrideOffline) { if (categoriesChanged > System.currentTimeMillis() - Utils.UPDATE_TIME) { return null; } else if (Utils.isConnected(cm) || overrideOffline) { Set<Category> categories = Controller.getInstance().getConnector().getCategories(); if (!categories.isEmpty()) { DBHelper.getInstance().deleteCategories(false); DBHelper.getInstance().insertCategories(categories); categoriesChanged = System.currentTimeMillis(); notifyListeners(); } return categories; } return null; } // *** STATUS ******************************************************************* public void setArticleRead(Set<Integer> ids, int status) { boolean erg = false; if (Utils.isConnected(cm)) erg = Controller.getInstance().getConnector().setArticleRead(ids, status); if (!erg) DBHelper.getInstance().markUnsynchronizedStates(ids, DBHelper.MARK_READ, status); } public void setArticleStarred(int articleId, int status) { boolean erg = false; Set<Integer> ids = new HashSet<>(); ids.add(articleId); if (Utils.isConnected(cm)) erg = Controller.getInstance().getConnector().setArticleStarred(ids, status); if (!erg) DBHelper.getInstance().markUnsynchronizedStates(ids, DBHelper.MARK_STAR, status); } public void setArticlePublished(int articleId, int status) { boolean erg = false; Set<Integer> ids = new HashSet<>(); ids.add(articleId); if (Utils.isConnected(cm)) erg = Controller.getInstance().getConnector().setArticlePublished(ids, status); if (!erg) DBHelper.getInstance().markUnsynchronizedStates(ids, DBHelper.MARK_PUBLISH, status); } public void setArticleNote(int articleId, String note) { boolean erg = false; Map<Integer, String> ids = new HashMap<>(); ids.put(articleId, note); if (Utils.isConnected(cm)) erg = Controller.getInstance().getConnector().setArticleNote(ids); if (!erg) DBHelper.getInstance().markUnsynchronizedNotes(ids); } /** * mark all articles in given category/feed as read * * @param id category/feed ID * @param isCategory if set to {@code true}, then given id is category * ID, otherwise - feed ID */ public void setRead(int id, boolean isCategory) { Collection<Integer> markedArticleIds = DBHelper.getInstance().markRead(id, isCategory); if (markedArticleIds != null) { boolean isSync = false; if (Utils.isConnected(cm)) isSync = Controller.getInstance().getConnector().setRead(id, isCategory); if (!isSync) DBHelper.getInstance().markUnsynchronizedStates(markedArticleIds, DBHelper.MARK_READ, 0); } } public boolean shareToPublished(String title, String url, String content) { return Utils.isConnected(cm) && Controller.getInstance().getConnector().shareToPublished(title, url, content); } public JSONConnector.SubscriptionResponse feedSubscribe(String feed_url, int category_id) { if (Utils.isConnected(cm)) return Controller.getInstance().getConnector().feedSubscribe(feed_url, category_id); return null; } public boolean feedUnsubscribe(int feed_id) { return Utils.isConnected(cm) && Controller.getInstance().getConnector().feedUnsubscribe(feed_id); } String getPref(String pref) { if (Utils.isConnected(cm)) return Controller.getInstance().getConnector().getPref(pref); return null; } public Set<Label> getLabels(int articleId) { return DBHelper.getInstance().getLabelsForArticle(articleId); } public boolean setLabel(Integer articleId, Label label) { Set<Integer> set = new HashSet<>(); set.add(articleId); return setLabel(set, label); } private boolean setLabel(Set<Integer> articleIds, Label label) { DBHelper.getInstance().insertLabels(articleIds, label, label.checked); notifyListeners(); boolean erg = false; if (Utils.isConnected(cm)) { Log.d(TAG, "Calling connector with Label: " + label + ") and ids.size() " + articleIds.size()); erg = Controller.getInstance().getConnector().setArticleLabel(articleIds, label.id, label.checked); } return erg; } /** * syncronize read, starred, published articles and notes with server */ public void synchronizeStatus() { if (!Utils.isConnected(cm)) return; long time = System.currentTimeMillis(); // Try to send all marked articles to the server, every synced status is removed from the DB afterwards String[] marks = new String[]{DBHelper.MARK_READ, DBHelper.MARK_STAR, DBHelper.MARK_PUBLISH}; for (String mark : marks) { Set<Integer> idsMark = DBHelper.getInstance().getMarked(mark, 1); Set<Integer> idsUnmark = DBHelper.getInstance().getMarked(mark, 0); if (DBHelper.MARK_READ.equals(mark)) { if (Controller.getInstance().getConnector().setArticleRead(idsMark, 1)) DBHelper.getInstance().setMarked(idsMark, mark); if (Controller.getInstance().getConnector().setArticleRead(idsUnmark, 0)) DBHelper.getInstance().setMarked(idsUnmark, mark); } if (DBHelper.MARK_STAR.equals(mark)) { if (Controller.getInstance().getConnector().setArticleStarred(idsMark, 1)) DBHelper.getInstance().setMarked(idsMark, mark); if (Controller.getInstance().getConnector().setArticleStarred(idsUnmark, 0)) DBHelper.getInstance().setMarked(idsUnmark, mark); } if (DBHelper.MARK_PUBLISH.equals(mark)) { if (Controller.getInstance().getConnector().setArticlePublished(idsMark, 1)) DBHelper.getInstance().setMarked(idsMark, mark); if (Controller.getInstance().getConnector().setArticlePublished(idsUnmark, 0)) DBHelper.getInstance().setMarked(idsUnmark, mark); } } // Try to send all article notes to the server, on success they are deleted from the DB Map<Integer, String> notesMarked = DBHelper.getInstance().getMarkedNotes(); if (notesMarked.size() > 0) { if (Controller.getInstance().getConnector().setArticleNote(notesMarked)) DBHelper.getInstance().setMarkedNotes(notesMarked); } Log.d(TAG, String.format("Syncing Status took %sms", (System.currentTimeMillis() - time))); } public void purgeOrphanedArticles() { if (Controller.getInstance().getLastCleanup() > System.currentTimeMillis() - Utils.CLEANUP_TIME) return; DBHelper.getInstance().purgeOrphanedArticles(); Controller.getInstance().setLastCleanup(System.currentTimeMillis()); } public void calculateCounters() { DBHelper.getInstance().calculateCounters(); } public void notifyListeners() { if (!Controller.getInstance().isHeadless()) UpdateController.getInstance().notifyListeners(); } public boolean isConnected() { return Utils.isConnected(cm); } /** * Deletes all database references on remotefiles and clears the cache folder. */ public void deleteAllRemoteFiles() { int count = DBHelper.getInstance().deleteAllRemoteFiles(); Log.w(TAG, String.format("Deleted %s Remotefiles from database.", count)); ImageCache cache = Controller.getInstance().getImageCache(); if (cache != null && cache.deleteAllCachedFiles()) Log.d(TAG, "Deleting cached files was successful."); else Log.e(TAG, "Deleting cached files failed at least partially, there were errors!"); } }