summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xorg.fox.ttrss/src/main/java/org/fox/ttrss/Application.java16
-rwxr-xr-xorg.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java8
-rwxr-xr-xorg.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java155
-rwxr-xr-xorg.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesLoader.java292
-rwxr-xr-xorg.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java4
5 files changed, 454 insertions, 21 deletions
diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/Application.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/Application.java
index 07f1a3b5..9ad069f1 100755
--- a/org.fox.ttrss/src/main/java/org/fox/ttrss/Application.java
+++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/Application.java
@@ -1,5 +1,8 @@
package org.fox.ttrss;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.os.Bundle;
import org.fox.ttrss.types.ArticleList;
@@ -17,6 +20,7 @@ public class Application extends android.app.Application {
private String m_sessionId;
private int m_apiLevel;
public LinkedHashMap<String, String> m_customSortModes = new LinkedHashMap<>();
+ ConnectivityManager m_cmgr;
public static Application getInstance(){
return m_singleton;
@@ -31,6 +35,7 @@ public class Application extends android.app.Application {
super.onCreate();
m_singleton = this;
+ m_cmgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
}
public String getSessionId() {
@@ -68,6 +73,15 @@ public class Application extends android.app.Application {
m_customSortModes.clear();
m_customSortModes.putAll(tmp);
}
-
}
+
+ public boolean isWifiConnected() {
+ NetworkInfo wifi = m_cmgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+
+ if (wifi != null)
+ return wifi.isConnected();
+
+ return false;
+ }
+
}
diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java
index da9dc8f3..46cf36ee 100755
--- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java
+++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticlePager.java
@@ -146,8 +146,12 @@ public class ArticlePager extends androidx.fragment.app.Fragment {
return view;
}
-
+
protected void refresh(final boolean append) {
+ //
+ }
+
+ /* protected void refresh(final boolean append) {
if (!append) {
m_lazyLoadDisabled = false;
@@ -291,7 +295,7 @@ public class ArticlePager extends androidx.fragment.app.Fragment {
Log.d(TAG, "[AP] request more headlines, firstId=" + m_firstId);
req.execute(map);
- }
+ } */
@Override
public void onAttach(@NonNull Activity activity) {
diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java
index ad023a7e..0030d2a6 100755
--- a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java
+++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java
@@ -44,11 +44,16 @@ import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.ViewCompat;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -84,7 +89,106 @@ import java.util.TimeZone;
import jp.wasabeef.glide.transformations.CropCircleTransformation;
-public class HeadlinesFragment extends androidx.fragment.app.Fragment {
+public class HeadlinesFragment extends androidx.fragment.app.Fragment implements LoaderManager.LoaderCallbacks<ArticleList> {
+
+ public class HeadlinesDiffutilCallback extends DiffUtil.Callback {
+ private ArticleList m_oldList;
+ private ArticleList m_newList;
+
+ public HeadlinesDiffutilCallback(ArticleList oldList, ArticleList newList) {
+ m_oldList = oldList;
+ m_newList = newList;
+ }
+
+ @Override
+ public int getOldListSize() {
+ return m_oldList != null ? m_oldList.size() : 0;
+ }
+
+ @Override
+ public int getNewListSize() {
+ return m_newList != null ? m_newList.size() : 0;
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return m_newList.get(newItemPosition).id == m_oldList.get(oldItemPosition).id;
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return false;
+ }
+ }
+
+
+ private HeadlinesLoader m_loader;
+
+ @NonNull
+ @Override
+ public Loader<ArticleList> onCreateLoader(int id, @Nullable Bundle args) {
+ return new HeadlinesLoader(getContext(), m_feed, m_activity.getResizeWidth());
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull Loader<ArticleList> loader, ArticleList data) {
+ Log.d(TAG, "onLoadFinished loader=" + loader + " count=" + data.size());
+
+ HeadlinesLoader headlinesLoader = (HeadlinesLoader) loader;
+
+ // successful update
+ if (data != null) {
+ ArticleList articles = Application.getArticles();
+
+ articles.stripFooters();
+
+ DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new HeadlinesDiffutilCallback(articles, data));
+
+ articles.clear();
+ articles.addAll(data);
+
+ diffResult.dispatchUpdatesTo(m_adapter);
+
+ // detail activity does not use footers (see above)
+ if (!(m_activity instanceof DetailActivity)) {
+ articles.add(new Article(Article.TYPE_AMR_FOOTER));
+ m_adapter.notifyItemInserted(articles.size());
+ }
+
+ if (!headlinesLoader.getAppend())
+ m_list.scrollToPosition(0);
+
+ //m_adapter.notifyDataSetChanged();
+
+ if (headlinesLoader.getFirstIdChanged()) {
+ //if (m_activity.isSmallScreen() || !m_activity.isPortrait()) {
+ Snackbar.make(getView(), R.string.headlines_row_top_changed, Snackbar.LENGTH_LONG)
+ .setAction(R.string.reload, v -> refresh(false)).show();
+ //}
+ }
+
+ } else {
+ if (headlinesLoader.getLastError() == ApiCommon.ApiError.LOGIN_FAILED) {
+ m_activity.login();
+ } else {
+
+ if (headlinesLoader.getLastErrorMessage() != null) {
+ m_activity.toast(m_activity.getString(headlinesLoader.getErrorMessage()) + "\n" + headlinesLoader.getLastErrorMessage());
+ } else {
+ m_activity.toast(headlinesLoader.getErrorMessage());
+ }
+ }
+
+ }
+
+ if (m_swipeLayout != null)
+ m_swipeLayout.setRefreshing(false);
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull Loader<ArticleList> loader) {
+
+ }
public enum ArticlesSelection { ALL, NONE, UNREAD }
@@ -98,7 +202,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
private String m_searchQuery = "";
private boolean m_refreshInProgress = false;
private int m_firstId = 0;
- private boolean m_lazyLoadDisabled = false;
+ //private boolean m_lazyLoadDisabled = false;
private SharedPreferences m_prefs;
@@ -251,7 +355,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
m_activeArticleId = savedInstanceState.getInt("m_activeArticleId");
m_searchQuery = savedInstanceState.getString("m_searchQuery");
m_firstId = savedInstanceState.getInt("m_firstId");
- m_lazyLoadDisabled = savedInstanceState.getBoolean("m_lazyLoadDisabled");
m_compactLayoutMode = savedInstanceState.getBoolean("m_compactLayoutMode");
}
@@ -268,7 +371,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
out.putInt("m_activeArticleId", m_activeArticleId);
out.putString("m_searchQuery", m_searchQuery);
out.putInt("m_firstId", m_firstId);
- out.putBoolean("m_lazyLoadDisabled", m_lazyLoadDisabled);
out.putBoolean("m_compactLayoutMode", m_compactLayoutMode);
}
@@ -287,7 +389,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
m_swipeLayout = view.findViewById(R.id.headlines_swipe_container);
- m_swipeLayout.setOnRefreshListener(() -> refresh(false, true));
+ m_swipeLayout.setOnRefreshListener(() -> refresh(false));
m_list = view.findViewById(R.id.headlines_list);
registerForContextMenu(m_list);
@@ -394,6 +496,11 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
new Handler().postDelayed(() -> m_activity.refresh(false), 100);
}
+
+ int lastVisibleItem = m_layoutManager.findLastVisibleItemPosition();
+
+ if (lastVisibleItem >= Application.getArticles().size() - 5)
+ new Handler().postDelayed(() -> refresh(true), 100);
}
}
@@ -423,11 +530,13 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
}
}
- if (!m_refreshInProgress && !m_lazyLoadDisabled && lastVisibleItem >= Application.getArticles().size() - 5) {
+ /*if (!m_refreshInProgress && !m_lazyLoadDisabled && lastVisibleItem >= Application.getArticles().size() - 5) {
m_refreshInProgress = true;
new Handler().postDelayed(() -> refresh(true), 100);
- }
+ }*/
+ /* if (lastVisibleItem >= Application.getArticles().size() - 5)
+ new Handler().postDelayed(() -> refresh(true), 100); */
}
});
@@ -444,7 +553,9 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
public void onResume() {
super.onResume();
- if (Application.getArticles().isEmpty()) {
+ m_loader = (HeadlinesLoader) LoaderManager.getInstance(this).initLoader(0, null, this);
+
+ if (Application.getArticles().getSizeWithoutFooters() == 0) {
refresh(false);
} else {
Article activeArticle = Application.getArticles().getById(m_activeArticleId);
@@ -464,11 +575,20 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
m_listener = (HeadlinesEventListener) activity;
}
- public void refresh(boolean append) {
- refresh(append, false);
+ public void refresh(final boolean append) {
+
+ if (!(m_activity instanceof DetailActivity)) {
+ // detail activity does not use footers because it would break 1-to-1 mapping with pager view
+ // pager will need to work on a footerless subset of shared article view before this is possible
+
+ Application.getArticles().add(new Article(Article.TYPE_LOADMORE));
+ m_adapter.notifyDataSetChanged();
+ }
+
+ m_loader.refresh(append);
}
- public void refresh(final boolean append, boolean userInitiated) {
+ /* public void __refresh(final boolean append) {
Application.getArticles().stripFooters();
m_adapter.notifyDataSetChanged();
@@ -557,10 +677,9 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
final int skip = getSkip(append);
final boolean allowForceUpdate = m_activity.getApiLevel() >= 9 &&
- !m_feed.is_cat && m_feed.id > 0 && !append && userInitiated &&
- skip == 0;
+ !m_feed.is_cat && m_feed.id > 0 && !append && skip == 0;
- Log.d(TAG, "allowForceUpdate=" + allowForceUpdate + " userInitiated=" + userInitiated + " skip=" + skip);
+ Log.d(TAG, "allowForceUpdate=" + allowForceUpdate + " skip=" + skip);
req.setOffset(skip);
@@ -636,7 +755,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
}
return skip;
- }
+ } */
static class ArticleViewHolder extends RecyclerView.ViewHolder {
public View view;
@@ -1531,11 +1650,13 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment {
}
public void setSelection(ArticlesSelection select) {
- for (Article a : Application.getArticles())
+ ArticleList articlesWithoutFooters = Application.getArticles().getWithoutFooters();
+
+ for (Article a : articlesWithoutFooters)
a.selected = false;
if (select != ArticlesSelection.NONE) {
- for (Article a : Application.getArticles()) {
+ for (Article a : articlesWithoutFooters) {
if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) {
a.selected = true;
}
diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesLoader.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesLoader.java
new file mode 100755
index 00000000..5cd7503c
--- /dev/null
+++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesLoader.java
@@ -0,0 +1,292 @@
+package org.fox.ttrss;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.loader.content.AsyncTaskLoader;
+import androidx.preference.PreferenceManager;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+
+import org.fox.ttrss.ApiCommon.ApiError;
+import org.fox.ttrss.types.Article;
+import org.fox.ttrss.types.ArticleList;
+import org.fox.ttrss.types.Feed;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+
+public class HeadlinesLoader extends AsyncTaskLoader<ArticleList> implements ApiCommon.ApiCaller {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private final int m_responseCode = 0;
+ protected String m_responseMessage;
+ private int m_apiStatusCode = 0;
+
+ private Context m_context;
+ private String m_lastErrorMessage;
+ private ApiError m_lastError;
+ private ArticleList m_articles;
+ private Feed m_feed;
+ private SharedPreferences m_prefs;
+ private int m_firstId;
+ private String m_searchQuery = "";
+ private boolean m_firstIdChanged;
+ private int m_offset;
+ private int m_amountLoaded;
+ private int m_resizeWidth;
+ private boolean m_append;
+ private boolean m_lazyLoadEnabled;
+ private boolean m_loadingInProgress;
+
+ HeadlinesLoader(Context context, Feed feed, int resizeWidth) {
+ super(context);
+
+ m_context = context;
+ m_lastError = ApiError.NO_ERROR;
+ m_feed = feed;
+ m_articles = new ArticleList();
+ m_resizeWidth = resizeWidth;
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ protected void refresh(boolean append) {
+ Log.d(TAG, "refresh, append=" + append + " inProgress=" + m_loadingInProgress + " lazyLoadEnabled=" + m_lazyLoadEnabled);
+
+ if (!append) {
+ m_append = false;
+ m_lazyLoadEnabled = true;
+
+ forceLoad();
+ } else if (!m_loadingInProgress && m_lazyLoadEnabled) {
+ m_append = true;
+ forceLoad();
+ } else {
+ deliverResult(m_articles);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (m_articles != null) {
+ deliverResult(m_articles);
+ } else {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public void deliverResult(ArticleList data) {
+ m_articles = data;
+
+ super.deliverResult(m_articles);
+ }
+
+ public int getErrorMessage() {
+ return ApiCommon.getErrorMessage(m_lastError);
+ }
+
+ ApiError getLastError() {
+ return m_lastError;
+ }
+
+ String getLastErrorMessage() {
+ return m_lastErrorMessage;
+ }
+
+ public boolean lazyLoadEnabled() {
+ return m_lazyLoadEnabled;
+ }
+
+ @Override
+ public ArticleList loadInBackground() {
+
+ m_loadingInProgress = true;
+
+ final int skip = getSkip(m_append);
+ final boolean allowForceUpdate = Application.getInstance().getApiLevel() >= 9 &&
+ !m_feed.is_cat && m_feed.id > 0 && !m_append && skip == 0;
+
+ HashMap<String,String> params = new HashMap<>();
+
+ params.put("op", "getHeadlines");
+ params.put("sid", Application.getInstance().getSessionId());
+ params.put("feed_id", String.valueOf(m_feed.id));
+ params.put("show_excerpt", "true");
+ params.put("excerpt_length", String.valueOf(CommonActivity.EXCERPT_MAX_LENGTH));
+ params.put("show_content", "true");
+ params.put("include_attachments", "true");
+ params.put("view_mode", m_prefs.getString("view_mode", "adaptive"));
+ params.put("limit", m_prefs.getString("headlines_request_size", "15"));
+ params.put("skip", String.valueOf(skip));
+ params.put("include_nested", "true");
+ params.put("has_sandbox", "true");
+ params.put("order_by", m_prefs.getString("headlines_sort_mode", "default"));
+
+ if (m_prefs.getBoolean("enable_image_downsampling", false)) {
+ if (m_prefs.getBoolean("always_downsample_images", false) || !Application.getInstance().isWifiConnected()) {
+ params.put("resize_width", String.valueOf(m_resizeWidth));
+ }
+ }
+
+ if (m_feed.is_cat)
+ params.put("is_cat", "true");
+
+ if (allowForceUpdate) {
+ params.put("force_update", "true");
+ }
+
+ if (m_searchQuery != null && !m_searchQuery.isEmpty()) {
+ params.put("search", m_searchQuery);
+ params.put("search_mode", "");
+ params.put("match_on", "both");
+ }
+
+ if (m_firstId > 0)
+ params.put("check_first_id", String.valueOf(m_firstId));
+
+ if (Application.getInstance().getApiLevel() >= 12) {
+ params.put("include_header", "true");
+ }
+
+ Log.d(TAG, "request more headlines, firstId=" + m_firstId + ", append=" + m_append + ", skip=" + skip);
+
+ JsonElement result = ApiCommon.performRequest(m_context, params, this);
+
+ Log.d(TAG, "got result=" + result);
+
+ if (result != null) {
+ try {
+ JsonArray content = result.getAsJsonArray();
+ if (content != null) {
+ final List<Article> articlesJson;
+ final JsonObject header;
+
+ if (Application.getInstance().getApiLevel() >= 12) {
+ header = content.get(0).getAsJsonObject();
+
+ m_firstIdChanged = header.get("first_id_changed") != null;
+
+ try {
+ m_firstId = header.get("first_id").getAsInt();
+ } catch (NumberFormatException e) {
+ m_firstId = 0;
+ }
+
+ Log.d(TAG, "firstID=" + m_firstId + " firstIdChanged=" + m_firstIdChanged);
+
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ articlesJson = new Gson().fromJson(content.get(1), listType);
+ } else {
+ Type listType = new TypeToken<List<Article>>() {}.getType();
+ articlesJson = new Gson().fromJson(content, listType);
+ }
+
+ if (skip == 0)
+ m_articles.clear();
+ /* else
+ m_articles.stripFooters(); */
+
+ m_amountLoaded = articlesJson.size();
+
+ for (Article f : articlesJson)
+ if (!m_articles.containsId(f.id)) {
+ f.collectMediaInfo();
+ f.cleanupExcerpt();
+ m_articles.add(f);
+ }
+
+ if (m_firstIdChanged) {
+ Log.d(TAG, "first id changed, disabling lazy load");
+ m_lazyLoadEnabled = false;
+ }
+
+ if (m_amountLoaded < Integer.parseInt(m_prefs.getString("headlines_request_size", "15"))) {
+ Log.d(TAG, "amount loaded "+m_amountLoaded+" < request size, disabling lazy load");
+ m_lazyLoadEnabled = false;
+ }
+
+ m_loadingInProgress = false;
+
+ return m_articles;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ m_loadingInProgress = false;
+
+ return null;
+
+ /* TODO move to onLoaderFinished() if (m_lastError == ApiCommon.ApiError.LOGIN_FAILED) {
+ m_activity.login();
+ } else {
+
+ if (m_lastErrorMessage != null) {
+ m_activity.toast(m_activity.getString(getErrorMessage()) + "\n" + m_lastErrorMessage);
+ } else {
+ m_activity.toast(getErrorMessage());
+ }
+ //m_activity.setLoadingStatus(getErrorMessage(), false);
+ } */
+ }
+
+ private int getSkip(boolean append) {
+ int skip = 0;
+
+ if (append) {
+ // adaptive, all_articles, marked, published, unread
+ String viewMode = m_prefs.getString("view_mode", "adaptive");
+
+ int numUnread = Math.toIntExact(m_articles.getUnreadCount());
+ int numAll = Math.toIntExact(m_articles.getSizeWithoutFooters());
+
+ if ("marked".equals(viewMode)) {
+ skip = numAll;
+ } else if ("published".equals(viewMode)) {
+ skip = numAll;
+ } else if ("unread".equals(viewMode)) {
+ skip = numUnread;
+ } else if (m_searchQuery != null && !m_searchQuery.isEmpty()) {
+ skip = numAll;
+ } else if ("adaptive".equals(viewMode)) {
+ skip = numUnread > 0 ? numUnread : numAll;
+ } else {
+ skip = numAll;
+ }
+ }
+
+ return skip;
+ }
+
+ @Override
+ public void setStatusCode(int statusCode) {
+ m_apiStatusCode = statusCode;
+ }
+
+ @Override
+ public void setLastError(ApiError lastError) {
+ m_lastError = lastError;
+ }
+
+ @Override
+ public void setLastErrorMessage(String message) {
+ m_lastErrorMessage = message;
+ }
+
+ public boolean getFirstIdChanged() {
+ return m_firstIdChanged;
+ }
+
+ public boolean getAppend() {
+ return m_append;
+ }
+}
diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java
index 7d476ad6..ec620337 100755
--- a/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java
+++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/Article.java
@@ -3,6 +3,8 @@ package org.fox.ttrss.types;
import android.os.Parcel;
import android.os.Parcelable;
+import androidx.annotation.NonNull;
+
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -268,7 +270,7 @@ public class Article implements Parcelable {
return false;
}
}
-
+
@SuppressWarnings("rawtypes")
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {