diff options
Diffstat (limited to 'org.fox.ttrss/src')
35 files changed, 925 insertions, 802 deletions
diff --git a/org.fox.ttrss/src/main/AndroidManifest.xml b/org.fox.ttrss/src/main/AndroidManifest.xml index feb8a2e1..3388d515 100755 --- a/org.fox.ttrss/src/main/AndroidManifest.xml +++ b/org.fox.ttrss/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:name=".Application" android:allowBackup="true" android:hardwareAccelerated="true" - android:icon="@mipmap/ic_launcher" + android:icon="${appIcon}" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" > @@ -217,7 +217,7 @@ <provider android:name="androidx.core.content.FileProvider" - android:authorities="org.fox.ttrss.SharedFileProvider" + android:authorities="${applicationId}.SharedFileProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java index 75963c96..adc0881d 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java @@ -41,13 +41,16 @@ public class ApiCommon { void setLastErrorMessage(String message); } - public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, + public enum ApiError { + SUCCESS, UNKNOWN_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, SSL_HOSTNAME_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED, INVALID_URL, API_INCORRECT_USAGE, NETWORK_UNAVAILABLE, API_UNKNOWN_METHOD } public static int getErrorMessage(ApiError error) { switch (error) { - case NO_ERROR: + case SUCCESS: + return R.string.error_success; + case UNKNOWN_ERROR: return R.string.error_unknown; case HTTP_UNAUTHORIZED: return R.string.error_http_unauthorized; @@ -154,6 +157,7 @@ public class ApiCommon { switch (statusCode) { case API_STATUS_OK: + caller.setLastError(ApiError.SUCCESS); return result.getAsJsonObject().get("content"); case API_STATUS_ERR: JsonObject contentObj = resultObj.get("content").getAsJsonObject(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiLoader.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiLoader.java index 259d7a28..8d83e7bd 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiLoader.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiLoader.java @@ -27,7 +27,7 @@ public class ApiLoader extends AsyncTaskLoader<JsonElement> implements ApiCommon super(context); m_context = context; - m_lastError = ApiError.NO_ERROR; + m_lastError = ApiError.UNKNOWN_ERROR; m_params = params; } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java index 409047f0..ba8dad20 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiRequest.java @@ -21,7 +21,7 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE public ApiRequest(Context context) { m_context = context; - m_lastError = ApiError.NO_ERROR; + m_lastError = ApiError.UNKNOWN_ERROR; } @SuppressLint("NewApi") 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..75542e3c 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; @@ -8,22 +11,28 @@ import java.util.HashMap; import java.util.LinkedHashMap; public class Application extends android.app.Application { - private static Application m_singleton; + public static final int LOADER_HEADLINES = 0; + public static final int LOADER_FEEDS = 1; + public static final int LOADER_CATS = 2; - // this is the only instance of a (large) object which contains all currently loaded articles and is - // used by all fragments and activities concurrently - private final ArticleList m_articles = new ArticleList(); + private static Application m_singleton; private String m_sessionId; private int m_apiLevel; public LinkedHashMap<String, String> m_customSortModes = new LinkedHashMap<>(); + ConnectivityManager m_cmgr; + ArticleModel m_articleModel; public static Application getInstance(){ return m_singleton; } public static ArticleList getArticles() { - return getInstance().m_articles; + return getInstance().m_articleModel.getArticles().getValue(); + } + + public static ArticleModel getArticlesModel() { + return getInstance().m_articleModel; } @Override @@ -31,6 +40,8 @@ public class Application extends android.app.Application { super.onCreate(); m_singleton = this; + m_cmgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + m_articleModel = new ArticleModel(this); } public String getSessionId() { @@ -68,6 +79,14 @@ 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/ArticleFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleFragment.java index 3ee42976..8794e653 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleFragment.java @@ -157,7 +157,7 @@ public class ArticleFragment extends androidx.fragment.app.Fragment { View noteContainer = view.findViewById(R.id.note_container); if (note != null && noteContainer != null) { - if (m_article.note != null && !m_article.note.isEmpty()) { + if (!m_article.note.isEmpty()) { note.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_articleSmallFontSize); note.setText(m_article.note); noteContainer.setVisibility(View.VISIBLE); @@ -259,7 +259,7 @@ public class ArticleFragment extends androidx.fragment.app.Fragment { String linkHexColor = String.format("#%06X", (0xFFFFFF & tvColorPrimary.data)); cssOverride += " a:link {color: "+linkHexColor+";} a:visited { color: "+linkHexColor+";}"; - String articleContent = m_article.content != null ? m_article.content : ""; + String articleContent = m_article.content; ws.setJavaScriptEnabled(false); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java new file mode 100644 index 00000000..80467f63 --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java @@ -0,0 +1,315 @@ +package org.fox.ttrss; + +import android.app.Application; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +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.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; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ArticleModel extends AndroidViewModel implements ApiCommon.ApiCaller { + private final String TAG = this.getClass().getSimpleName(); + private final MutableLiveData<ArticleList> m_articles = new MutableLiveData<>(new ArticleList()); + private SharedPreferences m_prefs; + private final int m_responseCode = 0; + protected String m_responseMessage; + private int m_apiStatusCode = 0; + + private String m_lastErrorMessage; + private ApiCommon.ApiError m_lastError; + private Feed m_feed; + 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 = true; + private boolean m_loadingInProgress; + private ExecutorService m_executor; + private Handler m_mainHandler = new Handler(Looper.getMainLooper()); + private MutableLiveData<Long> m_lastUpdate = new MutableLiveData<>(Long.valueOf(0)); + + public ArticleModel(@NonNull Application application) { + super(application); + + m_prefs = PreferenceManager.getDefaultSharedPreferences(application); + + // do we need concurrency or not? + m_executor = Executors.newSingleThreadExecutor(); + } + + public LiveData<Long> getUpdatesData() { + return m_lastUpdate; + } + + public LiveData<ArticleList> getArticles() { + return m_articles; + } + + + public void update(int position, Article article) { + m_articles.getValue().set(position, article); + m_articles.postValue(m_articles.getValue()); + } + + public void update(ArticleList articles) { + m_articles.postValue(articles); + } + + public void startLoading(boolean append, @NonNull Feed feed, int resizeWidth) { + Log.d(TAG, "startLoading append=" + append); + + m_resizeWidth = resizeWidth; + + if (!append) { + m_append = false; + m_lazyLoadEnabled = true; + m_feed = feed; + + loadInBackground(); + } else if (feed != m_feed || m_lazyLoadEnabled && !m_loadingInProgress) { + m_append = true; + m_feed = feed; + + loadInBackground(); + } else { + m_articles.postValue(m_articles.getValue()); + } + } + + private void loadInBackground() { + Log.d(TAG, this + " loadInBackground append=" + m_append + " offset=" + m_offset); + + ArticleList articlesWork = new ArticleList(m_articles.getValue()); + + m_loadingInProgress = true; + + final int skip = getSkip(m_append, articlesWork); + final boolean allowForceUpdate = org.fox.ttrss.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", org.fox.ttrss.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) || !org.fox.ttrss.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 (org.fox.ttrss.Application.getInstance().getApiLevel() >= 12) { + params.put("include_header", "true"); + } + + Log.d(TAG, "firstId=" + m_firstId + " append=" + m_append + " skip=" + skip + " localSize=" + articlesWork.size()); + + m_executor.execute(() -> { + JsonElement result = ApiCommon.performRequest(getApplication(), 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 (org.fox.ttrss.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, this + " 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 (!m_append) + articlesWork.clear(); + + m_amountLoaded = articlesJson.size(); + + for (Article article : articlesJson) + if (!articlesWork.containsId(article.id)) { + article.collectMediaInfo(); + article.cleanupExcerpt(); + article.fixNullFields(); + articlesWork.add(article); + } + + 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, this + " amount loaded "+m_amountLoaded+" < request size, disabling lazy load"); + m_lazyLoadEnabled = false; + } + + m_offset += m_amountLoaded; + m_loadingInProgress = false; + + Log.d(TAG, this + " loaded headlines=" + m_amountLoaded + " resultingLocalSize=" + articlesWork.size()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + m_mainHandler.post(() -> { + m_articles.setValue(articlesWork); + m_lastUpdate.setValue(System.currentTimeMillis()); + }); + }); + + m_loadingInProgress = false; + + } + + private int getSkip(boolean append, ArticleList articles) { + int skip = 0; + + if (append) { + // adaptive, all_articles, marked, published, unread + String viewMode = m_prefs.getString("view_mode", "adaptive"); + + int numUnread = Math.toIntExact(articles.getUnreadCount()); + int numAll = Math.toIntExact(articles.size()); + + 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(ApiCommon.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; + } + + public void setSearchQuery(String searchQuery) { + m_searchQuery = searchQuery; + } + + public String getSearchQuery() { + return m_searchQuery; + } + + public int getOffset() { + return m_offset; + } + + public boolean lazyLoadEnabled() { + return m_lazyLoadEnabled; + } + + public int getErrorMessage() { + return ApiCommon.getErrorMessage(m_lastError); + } + + ApiCommon.ApiError getLastError() { + return m_lastError; + } + + String getLastErrorMessage() { + return m_lastErrorMessage; + } + + public boolean isLoading() { + return m_loadingInProgress; + } +} 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..cf43f4f0 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 @@ -2,10 +2,7 @@ package org.fox.ttrss; import android.annotation.SuppressLint; import android.app.Activity; -import android.content.SharedPreferences; -import android.os.BadParcelableException; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -13,65 +10,44 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.preference.PreferenceManager; -import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; -import com.google.android.material.snackbar.Snackbar; -import com.google.gson.JsonElement; - import org.fox.ttrss.types.Article; +import org.fox.ttrss.types.ArticleList; import org.fox.ttrss.types.Feed; -import org.fox.ttrss.util.HeadlinesRequest; - -import java.util.HashMap; +import org.fox.ttrss.util.DiffFragmentStateAdapter; +import org.fox.ttrss.util.ArticleDiffItemCallback; public class ArticlePager extends androidx.fragment.app.Fragment { - private final String TAG = "ArticlePager"; + private final String TAG = this.getClass().getSimpleName(); private PagerAdapter m_adapter; private HeadlinesEventListener m_listener; private int m_articleId; private OnlineActivity m_activity; - private String m_searchQuery = ""; private Feed m_feed; - private SharedPreferences m_prefs; - private int m_firstId = 0; - private boolean m_refreshInProgress; - private boolean m_lazyLoadDisabled; private ViewPager2 m_pager; - private static class PagerAdapter extends FragmentStateAdapter { - - public PagerAdapter(FragmentActivity fa) { - super(fa); + private static class PagerAdapter extends DiffFragmentStateAdapter<Article> { + + public PagerAdapter(@NonNull Fragment fragment) { + super(fragment, new ArticleDiffItemCallback()); + } + + private void syncToSharedArticles() { + submitList(new ArticleList(Application.getArticles())); } @Override @NonNull public Fragment createFragment(int position) { - try { - Article article = Application.getArticles().get(position); - - if (article != null) { - ArticleFragment af = new ArticleFragment(); - af.initialize(article); - - return af; - } - } catch (IndexOutOfBoundsException e) { - e.printStackTrace(); - } + Article article = getItem(position); - return null; - } + ArticleFragment af = new ArticleFragment(); + af.initialize(article); - @Override - public int getItemCount() { - return Application.getArticles().size(); + return af; } - } public void initialize(int articleId, Feed feed) { @@ -79,17 +55,12 @@ public class ArticlePager extends androidx.fragment.app.Fragment { m_feed = feed; } - public void setSearchQuery(String searchQuery) { - m_searchQuery = searchQuery; - } - @Override public void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); out.putInt("m_articleId", m_articleId); out.putParcelable("m_feed", m_feed); - out.putInt("m_firstId", m_firstId); } @Override @@ -99,7 +70,6 @@ public class ArticlePager extends androidx.fragment.app.Fragment { if (savedInstanceState != null) { m_articleId = savedInstanceState.getInt("m_articleId"); m_feed = savedInstanceState.getParcelable("m_feed"); - m_firstId = savedInstanceState.getInt("m_firstId"); } setRetainInstance(true); @@ -109,36 +79,42 @@ public class ArticlePager extends androidx.fragment.app.Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_article_pager, container, false); - m_adapter = new PagerAdapter(getActivity()); + m_adapter = new PagerAdapter(this); + m_adapter.submitList(Application.getArticles()); + + ArticleModel model = Application.getArticlesModel(); + + // deal with further updates + model.getArticles().observe(getActivity(), articles -> { + Log.d(TAG, "observed article list size=" + articles.size()); + m_adapter.submitList(articles); + }); m_pager = view.findViewById(R.id.article_pager); - int position = Application.getArticles().getPositionById(m_articleId); - m_listener.onArticleSelected(Application.getArticles().getById(m_articleId), false); m_pager.setAdapter(m_adapter); m_pager.setOffscreenPageLimit(3); - m_pager.setCurrentItem(position, false); + int position = Application.getArticles().getPositionById(m_articleId); + + if (position != -1) + m_pager.setCurrentItem(position, false); + m_pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { Log.d(TAG, "onPageSelected: " + position); - final Article article = Application.getArticles().get(position); - - if (article != null) { - m_articleId = article.id; - - new Handler().postDelayed(() -> m_listener.onArticleSelected(article, false), 250); + // wtf + if (position != -1) { + Article article = Application.getArticles().get(position); - //Log.d(TAG, "Page #" + position + "/" + m_adapter.getCount()); + if (article != null) { + m_articleId = article.id; - if (!m_refreshInProgress && !m_lazyLoadDisabled && (m_activity.isSmallScreen() || m_activity.isPortrait()) && position >= m_adapter.getItemCount() - 5) { - Log.d(TAG, "loading more articles..."); - - new Handler().postDelayed(() -> refresh(true), 100); + m_listener.onArticleSelected(article, false); } } } @@ -146,170 +122,19 @@ public class ArticlePager extends androidx.fragment.app.Fragment { return view; } - - protected void refresh(final boolean append) { - - if (!append) { - m_lazyLoadDisabled = false; - } - - m_refreshInProgress = true; - - @SuppressLint("StaticFieldLeak") HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity, Application.getArticles()) { - @Override - protected void onPostExecute(JsonElement result) { - if (isDetached() || !isAdded()) return; - - if (!append) { - m_pager.setCurrentItem(0, false); - Application.getArticles().clear(); - } - - super.onPostExecute(result); - - m_refreshInProgress = false; - - if (result != null) { - - if (m_firstIdChanged) { - m_lazyLoadDisabled = true; - } - - if (m_firstIdChanged && !(m_activity instanceof DetailActivity && !m_activity.isPortrait())) { - //m_activity.toast(R.string.headlines_row_top_changed); - - Snackbar.make(getView(), R.string.headlines_row_top_changed, Snackbar.LENGTH_LONG) - .setAction(R.string.reload, v -> refresh(false)).show(); - } - - if (m_amountLoaded < Integer.parseInt(m_prefs.getString("headlines_request_size", "15"))) { - m_lazyLoadDisabled = true; - } - - ArticlePager.this.m_firstId = m_firstId; - - try { - m_adapter.notifyDataSetChanged(); - } catch (BadParcelableException e) { - if (getActivity() != null) { - getActivity().finish(); - return; - } - } - - if (!Application.getArticles().isEmpty()) { - if (Application.getArticles().getById(m_articleId) == null) { - Article article = Application.getArticles().get(0); - - m_articleId = article.id; - m_listener.onArticleSelected(article, false); - } - } - - } else { - m_lazyLoadDisabled = true; - - if (m_lastError == ApiCommon.ApiError.LOGIN_FAILED) { - m_activity.login(true); - } else { - m_activity.toast(getErrorMessage()); - } - } - } - }; - - final Feed feed = m_feed; - - final String sessionId = m_activity.getSessionId(); - int skip = 0; - - if (append) { - // adaptive, all_articles, marked, published, unread - String viewMode = m_activity.getViewMode(); - int numUnread = 0; - int numAll = Application.getArticles().size(); - - for (Article a : Application.getArticles()) { - if (a.unread) ++numUnread; - } - - 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; - } - } - - final int fskip = skip; - - req.setOffset(skip); - - HashMap<String,String> map = new HashMap<>(); - map.put("op", "getHeadlines"); - map.put("sid", sessionId); - map.put("feed_id", String.valueOf(feed.id)); - map.put("show_excerpt", "true"); - map.put("excerpt_length", String.valueOf(CommonActivity.EXCERPT_MAX_LENGTH)); - map.put("show_content", "true"); - map.put("include_attachments", "true"); - map.put("limit", m_prefs.getString("headlines_request_size", "15")); - map.put("offset", String.valueOf(0)); - map.put("view_mode", m_activity.getViewMode()); - map.put("skip", String.valueOf(fskip)); - map.put("include_nested", "true"); - map.put("has_sandbox", "true"); - map.put("order_by", m_activity.getSortMode()); - - if (feed.is_cat) map.put("is_cat", "true"); - - if (m_searchQuery != null && !m_searchQuery.isEmpty()) { - map.put("search", m_searchQuery); - map.put("search_mode", ""); - map.put("match_on", "both"); - } - - if (m_firstId > 0) map.put("check_first_id", String.valueOf(m_firstId)); - - if (m_activity.getApiLevel() >= 12) { - map.put("include_header", "true"); - } - - if (m_prefs.getBoolean("enable_image_downsampling", false)) { - if (m_prefs.getBoolean("always_downsample_images", false) || !m_activity.isWifiConnected()) { - map.put("resize_width", String.valueOf(m_activity.getResizeWidth())); - } - } - - Log.d(TAG, "[AP] request more headlines, firstId=" + m_firstId); - req.execute(map); - } - @Override public void onAttach(@NonNull Activity activity) { super.onAttach(activity); m_listener = (HeadlinesEventListener)activity; m_activity = (OnlineActivity)activity; - - m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); } @SuppressLint("NewApi") @Override public void onResume() { super.onResume(); - - //if (m_adapter != null) m_adapter.notifyDataSetChanged(); - m_activity.invalidateOptionsMenu(); } @@ -317,7 +142,8 @@ public class ArticlePager extends androidx.fragment.app.Fragment { if (m_pager != null && articleId != m_articleId) { int position = Application.getArticles().getPositionById(articleId); - m_pager.setCurrentItem(position, false); + if (position != -1) + m_pager.setCurrentItem(position, false); } } @@ -345,7 +171,13 @@ public class ArticlePager extends androidx.fragment.app.Fragment { return m_articleId; } - public void notifyUpdated() { - m_adapter.notifyDataSetChanged(); + public void notifyItemChanged(int position) { + if (m_adapter != null) + m_adapter.notifyItemChanged(position); + } + + public void syncToSharedArticles() { + if (m_adapter != null) + m_adapter.syncToSharedArticles(); } } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/BaseFeedlistFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/BaseFeedlistFragment.java index 86a31465..def61209 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/BaseFeedlistFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/BaseFeedlistFragment.java @@ -17,7 +17,7 @@ import java.net.URL; public abstract class BaseFeedlistFragment extends androidx.fragment.app.Fragment { abstract public void refresh(); - public void initDrawerHeader(LayoutInflater inflater, View view, ListView list, final CommonActivity activity, final SharedPreferences prefs, boolean isRoot) { + public void initDrawerHeader(LayoutInflater inflater, View view, ListView list, final CommonActivity activity, final SharedPreferences prefs) { View layout = inflater.inflate(R.layout.drawer_header, list, false); list.addHeaderView(layout, null, false); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java index cdc261b7..97e356c4 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java @@ -15,8 +15,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -66,7 +64,6 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc public final static String FRAG_ARTICLE = "article"; public final static String FRAG_FEEDS = "feeds"; public final static String FRAG_CATS = "cats"; - public final static String FRAG_DIALOG = "dialog"; public final static String THEME_DEFAULT = "THEME_FOLLOW_DEVICE"; @@ -74,7 +71,6 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc public final static String NOTIFICATION_CHANNEL_PRIORITY = "channel_priority"; public static final int EXCERPT_MAX_LENGTH = 256; - public static final int EXCERPT_MAX_QUERY_LENGTH = 2048; public static final int LABEL_BASE_INDEX = -1024; public static final int PENDING_INTENT_CHROME_SHARE = 1; @@ -330,22 +326,6 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc }); } - protected void preloadUriIfAllowed(Uri uri) { - boolean enableCustomTabs = m_prefs.getBoolean("enable_custom_tabs", true); - - if (m_customTabClient != null && enableCustomTabs) { - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - - if (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI) { - CustomTabsSession session = getCustomTabSession(); - session.mayLaunchUrl(uri, null, null); - - //toast("Preloading: " + uri.toString()); - } - } - } - protected Intent getShareIntent(String text, String subject) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java index 6558c2d1..3995014b 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java @@ -6,7 +6,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -94,7 +93,7 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList saveArticleUnread(article); if (hf != null) { - hf.notifyUpdated(); + hf.notifyItemChanged(Application.getArticles().indexOf(article)); } } } @@ -149,17 +148,16 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - final HeadlinesFragment hf = new HeadlinesFragment(); + HeadlinesFragment hf = new HeadlinesFragment(); hf.initialize(feed, openedArticleId, true); hf.setSearchQuery(searchQuery); ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES); - ArticlePager af = new ArticlePager(); - af.initialize(openedArticleId, feed); - af.setSearchQuery(searchQuery); + ArticlePager ap = new ArticlePager(); + ap.initialize(openedArticleId, feed); - ft.replace(R.id.article_fragment, af, FRAG_ARTICLE); + ft.replace(R.id.article_fragment, ap, FRAG_ARTICLE); ft.commit(); @@ -261,7 +259,7 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList } @Override - public void onArticleListSelectionChange(ArticleList m_selectedArticles) { + public void onArticleListSelectionChange() { invalidateOptionsMenu(); } @@ -271,35 +269,23 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList } @Override - public void onArticleSelected(final Article article, boolean open) { - - if (article == null) return; - + public void onArticleSelected(Article article, boolean open) { + if (article.unread) { article.unread = false; saveArticleUnread(article); } - try { - preloadUriIfAllowed(Uri.parse(article.link)); - } catch (Exception e) { - e.printStackTrace(); - } - if (!getSupportActionBar().isShowing()) getSupportActionBar().show(); - if (open) { - - new Handler().postDelayed(() -> { - ArticlePager ap = (ArticlePager) DetailActivity.this.getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - - if (ap != null) { - ap.setActiveArticleId(article.id); - } - }, 250); + ArticlePager ap = (ArticlePager) DetailActivity.this.getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); + HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); + if (open) { + if (ap != null) { + ap.setActiveArticleId(article.id); + } } else { - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); if (hf != null) { hf.setActiveArticleId(article.id); } @@ -313,11 +299,11 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - if (ap != null) { - ap.notifyUpdated(); + if (ap != null) { + ap.syncToSharedArticles(); } - if (hf != null) { + /* if (hf != null) { Article article = Application.getArticles().getById(hf.getActiveArticleId()); if (article == null && !Application.getArticles().isEmpty()) { @@ -335,7 +321,7 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList ft.replace(R.id.article_fragment, af, FRAG_ARTICLE); ft.commitAllowingStateLoss(); } - } + } */ } @Override diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java index 08a656e4..1d735490 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedCategoriesFragment.java @@ -62,9 +62,9 @@ public class FeedCategoriesFragment extends BaseFeedlistFragment implements OnIt params.put("op", "getCategories"); params.put("sid", sessionId); params.put("enable_nested", "true"); - if (unreadOnly) { - params.put("unread_only", String.valueOf(unreadOnly)); - } + + if (unreadOnly) + params.put("unread_only", "true"); return new ApiLoader(getContext(), params); } @@ -288,7 +288,7 @@ public class FeedCategoriesFragment extends BaseFeedlistFragment implements OnIt m_list = view.findViewById(R.id.feeds); m_adapter = new FeedCategoryListAdapter(getActivity(), R.layout.feeds_row, m_cats); - initDrawerHeader(inflater, view, m_list, m_activity, m_prefs, true); + initDrawerHeader(inflater, view, m_list, m_activity, m_prefs); m_list.setAdapter(m_adapter); m_list.setOnItemClickListener(this); @@ -312,7 +312,7 @@ public class FeedCategoriesFragment extends BaseFeedlistFragment implements OnIt public void onResume() { super.onResume(); - LoaderManager.getInstance(this).initLoader(0, null, this).forceLoad(); + LoaderManager.getInstance(this).initLoader(Application.LOADER_CATS, null, this).forceLoad(); m_activity.invalidateOptionsMenu(); } @@ -324,7 +324,7 @@ public class FeedCategoriesFragment extends BaseFeedlistFragment implements OnIt if (m_swipeLayout != null) m_swipeLayout.setRefreshing(true); - LoaderManager.getInstance(this).restartLoader(0, null, this).forceLoad(); + LoaderManager.getInstance(this).restartLoader(Application.LOADER_CATS, null, this).forceLoad(); } private class FeedCategoryListAdapter extends ArrayAdapter<FeedCategory> { diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java index 495180bb..f618b5b1 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/FeedsFragment.java @@ -327,7 +327,7 @@ public class FeedsFragment extends BaseFeedlistFragment implements OnItemClickLi m_list = view.findViewById(R.id.feeds); - initDrawerHeader(inflater, view, m_list, m_activity, m_prefs, !m_enableParentBtn); + initDrawerHeader(inflater, view, m_list, m_activity, m_prefs); if (m_enableParentBtn) { View layout = inflater.inflate(R.layout.feeds_goback, m_list, false); @@ -366,7 +366,7 @@ public class FeedsFragment extends BaseFeedlistFragment implements OnItemClickLi public void onResume() { super.onResume(); - LoaderManager.getInstance(this).initLoader(0, null, this).forceLoad(); + LoaderManager.getInstance(this).initLoader(Application.LOADER_FEEDS, null, this).forceLoad(); m_activity.invalidateOptionsMenu(); } @@ -404,7 +404,7 @@ public class FeedsFragment extends BaseFeedlistFragment implements OnItemClickLi m_swipeLayout.setRefreshing(true); } - LoaderManager.getInstance(this).restartLoader(0, null, this).forceLoad(); + LoaderManager.getInstance(this).restartLoader(Application.LOADER_FEEDS, null, this).forceLoad(); } private class FeedListAdapter extends ArrayAdapter<Feed> { diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java index 5494bb2b..27e00bbf 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesEventListener.java @@ -4,7 +4,7 @@ import org.fox.ttrss.types.Article; import org.fox.ttrss.types.ArticleList; public interface HeadlinesEventListener { - void onArticleListSelectionChange(ArticleList m_selectedArticles); + void onArticleListSelectionChange(); void onArticleSelected(Article article); void onArticleSelected(Article article, boolean open); void onHeadlinesLoaded(boolean appended); 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..d39ee038 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 @@ -1,6 +1,5 @@ package org.fox.ttrss; -import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.Context; @@ -51,6 +50,7 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -65,14 +65,13 @@ import com.bumptech.glide.request.target.Target; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; -import com.google.gson.JsonElement; import org.fox.ttrss.glide.ProgressTarget; import org.fox.ttrss.types.Article; import org.fox.ttrss.types.ArticleList; import org.fox.ttrss.types.Attachment; import org.fox.ttrss.types.Feed; -import org.fox.ttrss.util.HeadlinesRequest; +import org.fox.ttrss.util.ArticleDiffItemCallback; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -81,11 +80,19 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.TimeZone; +import java.util.stream.Collectors; import jp.wasabeef.glide.transformations.CropCircleTransformation; public class HeadlinesFragment extends androidx.fragment.app.Fragment { + private boolean m_isLazyLoading; + + public void notifyItemChanged(int position) { + if (m_adapter != null) + m_adapter.notifyItemChanged(position); + } + public enum ArticlesSelection { ALL, NONE, UNREAD } public static final int FLAVOR_IMG_MIN_SIZE = 128; @@ -96,9 +103,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { private Feed m_feed; private int m_activeArticleId; private String m_searchQuery = ""; - private boolean m_refreshInProgress = false; - private int m_firstId = 0; - private boolean m_lazyLoadDisabled = false; private SharedPreferences m_prefs; @@ -114,14 +118,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { private MediaPlayer m_mediaPlayer; private TextureView m_activeTexture; - public ArticleList getSelectedArticles() { - ArticleList tmp = new ArticleList(); + protected static HashMap<Integer, Integer> m_flavorHeightsCache = new HashMap<>(); - for (Article a : Application.getArticles()) { - if (a.selected) tmp.add(a); - } - - return tmp; + public ArticleList getSelectedArticles() { + return Application.getArticles() + .stream() + .filter(a -> a.selected).collect(Collectors.toCollection(ArticleList::new)); } public void initialize(Feed feed) { @@ -186,19 +188,24 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { private void catchupAbove(Article article) { ArticleList tmp = new ArticleList(); - for (Article a : Application.getArticles()) { + ArticleList articles = Application.getArticles(); + for (Article a : articles) { if (article.equalsById(a)) break; if (a.unread) { a.unread = false; tmp.add(a); + + int position = articles.getPositionById(a.id); + + if (position != -1) + m_adapter.notifyItemChanged(position); } } if (!tmp.isEmpty()) { m_activity.setArticlesUnread(tmp, Article.UPDATE_SET_FALSE); - m_adapter.notifyDataSetChanged(); } } @@ -250,8 +257,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_feed = savedInstanceState.getParcelable("m_feed"); 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"); } @@ -267,13 +272,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { out.putParcelable("m_feed", m_feed); 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); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Log.d(TAG, "onCreateView"); String headlineMode = m_prefs.getString("headline_mode", "HL_DEFAULT"); @@ -287,7 +291,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); @@ -296,11 +300,14 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_list.setLayoutManager(m_layoutManager); m_list.setItemAnimator(new DefaultItemAnimator()); - m_adapter = new ArticleListAdapter(Application.getArticles()); - + m_adapter = new ArticleListAdapter(); m_list.setAdapter(m_adapter); - if (m_prefs.getBoolean("headlines_swipe_to_dismiss", true) && !m_prefs.getBoolean("headlines_mark_read_scroll", false) ) { + if (savedInstanceState == null && Application.getArticles().isEmpty()) { + refresh(false); + } + + if (m_prefs.getBoolean("headlines_swipe_to_dismiss", true) /*&& !m_prefs.getBoolean("headlines_mark_read_scroll", false) */) { ItemTouchHelper swipeHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) { @@ -345,8 +352,10 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { wasUnread = false; } - Application.getArticles().remove(adapterPosition); - m_adapter.notifyItemRemoved(adapterPosition); + ArticleList tmpRemove = new ArticleList(Application.getArticles()); + tmpRemove.remove(adapterPosition); + + Application.getArticlesModel().update(tmpRemove); Snackbar.make(m_list, R.string.headline_undo_row_prompt, Snackbar.LENGTH_LONG) .setAction(getString(R.string.headline_undo_row_button), v -> { @@ -356,9 +365,10 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_activity.saveArticleUnread(article); } - Application.getArticles().add(adapterPosition, article); - m_adapter.notifyItemInserted(adapterPosition); - m_adapter.notifyItemRangeChanged(adapterPosition, 1); + ArticleList tmpInsert = new ArticleList(Application.getArticles()); + tmpInsert.add(adapterPosition, article); + + Application.getArticlesModel().update(tmpInsert); }).show(); } @@ -378,17 +388,22 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE && m_prefs.getBoolean("headlines_mark_read_scroll", false)) { - if (!m_readArticles.isEmpty()) { - m_activity.toggleArticlesUnread(m_readArticles); + ArticleModel model = Application.getArticlesModel(); - for (Article a : m_readArticles) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + if (!m_readArticles.isEmpty() && !m_isLazyLoading && !model.isLoading() && m_prefs.getBoolean("headlines_mark_read_scroll", false)) { + Log.d(TAG, "marking articles as read, count=" + m_readArticles.size()); + + m_activity.setArticlesUnread(m_readArticles, Article.UPDATE_SET_FALSE); + + for (Article a : m_readArticles) { a.unread = false; - if (m_feed != null) - m_feed.unread -= m_readArticles.size(); + int position = Application.getArticles().getPositionById(a.id); - m_adapter.notifyDataSetChanged(); + if (position != -1) + m_adapter.notifyItemChanged(position); + } m_readArticles.clear(); @@ -407,27 +422,33 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { // Log.d(TAG, "onScrolled: FVI=" + firstVisibleItem + " LVI=" + lastVisibleItem); if (m_prefs.getBoolean("headlines_mark_read_scroll", false)) { - for (int i = 0; i < firstVisibleItem; i++) { try { Article article = Application.getArticles().get(i); - if (article.unread && !m_readArticles.contains(article)) { - Log.d(TAG, "adding to mark read=" + article.title); - + if (article.unread && !m_readArticles.contains(article)) m_readArticles.add(article); - } + } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } } - } - if (!m_refreshInProgress && !m_lazyLoadDisabled && lastVisibleItem >= Application.getArticles().size() - 5) { - m_refreshInProgress = true; - new Handler().postDelayed(() -> refresh(true), 100); + // Log.d(TAG, "pending to auto mark as read count=" + m_readArticles.size()); } + ArticleModel model = Application.getArticlesModel(); + + if (dy > 0 && !m_isLazyLoading && !model.isLoading() && model.lazyLoadEnabled() && + lastVisibleItem >= Application.getArticles().size() - 5) { + + Log.d(TAG, "attempting to lazy load more articles..."); + + m_isLazyLoading = true; + + // this has to be dispatched delayed, consequent adapter updates are forbidden in scroll handler + new Handler().postDelayed(() -> refresh(true), 250); + } } }); @@ -435,212 +456,105 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_activity.setTitle(m_feed.title); } - Log.d(TAG, "onCreateView, feed=" + m_feed); + ArticleModel model = Application.getArticlesModel(); - return view; - } + // this gets notified on network update + model.getUpdatesData().observe(getActivity(), lastUpdate -> { + ArticleList tmp = new ArticleList(model.getArticles().getValue()); - @Override - public void onResume() { - super.onResume(); - - if (Application.getArticles().isEmpty()) { - refresh(false); - } else { - Article activeArticle = Application.getArticles().getById(m_activeArticleId); - - if (activeArticle != null) - scrollToArticle(activeArticle); - } + Log.d(TAG, "observed last update=" + lastUpdate + " article count=" + tmp.size()); - m_activity.invalidateOptionsMenu(); - } + if (m_prefs.getBoolean("headlines_mark_read_scroll", false)) + tmp.add(new Article(Article.TYPE_AMR_FOOTER)); - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); - m_activity = (OnlineActivity) activity; - m_listener = (HeadlinesEventListener) activity; - } + final boolean appended = model.getAppend(); - public void refresh(boolean append) { - refresh(append, false); - } + m_adapter.submitList(tmp, () -> { + if (!appended) + m_list.scrollToPosition(0); - public void refresh(final boolean append, boolean userInitiated) { - Application.getArticles().stripFooters(); - m_adapter.notifyDataSetChanged(); + if (m_swipeLayout != null) + m_swipeLayout.setRefreshing(false); - if (!append) m_lazyLoadDisabled = false; + m_isLazyLoading = false; - if (m_activity != null && isAdded() && m_feed != null) { - m_refreshInProgress = true; + m_listener.onHeadlinesLoaded(appended); + m_listener.onArticleListSelectionChange(); + }); - if (m_swipeLayout != null) m_swipeLayout.setRefreshing(true); + if (model.getFirstIdChanged()) + Snackbar.make(getView(), R.string.headlines_row_top_changed, Snackbar.LENGTH_LONG) + .setAction(R.string.reload, v -> refresh(false)).show(); - if (!append) { - m_activity.getSupportActionBar().show(); - Application.getArticles().clear(); - m_adapter.notifyDataSetChanged(); - } else 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(); + if (model.getLastError() == ApiCommon.ApiError.LOGIN_FAILED) { + m_activity.login(); + } else if (model.getLastError() != null && model.getLastError() != ApiCommon.ApiError.SUCCESS) { + if (model.getLastErrorMessage() != null) { + m_activity.toast(m_activity.getString(model.getErrorMessage()) + "\n" + model.getLastErrorMessage()); + } else { + m_activity.toast(model.getErrorMessage()); + } } - final String sessionId = m_activity.getSessionId(); - final boolean isCat = m_feed.is_cat; - - @SuppressLint("StaticFieldLeak") HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity, Application.getArticles()) { - @Override - protected void onPostExecute(JsonElement result) { - if (isDetached() || !isAdded()) return; - - super.onPostExecute(result); - - if (m_swipeLayout != null) m_swipeLayout.setRefreshing(false); - - m_refreshInProgress = false; - - if (result != null) { - - // is this needed? - if (!Application.getArticles().containsId(m_activeArticleId)) - m_activeArticleId = 0; - - if (m_firstIdChanged) { - m_lazyLoadDisabled = true; - - Log.d(TAG, "first id changed, disabling lazy load"); + }); - // article pager deals with this in tablet landscape view - 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(); - } - } + // loaded articles might get modified for all sorts of reasons + model.getArticles().observe(getActivity(), articles -> { + Log.d(TAG, "observed article list size=" + articles.size()); - 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_lazyLoadDisabled = true; - } + ArticleList tmp = new ArticleList(articles); - HeadlinesFragment.this.m_firstId = m_firstId; + if (m_prefs.getBoolean("headlines_mark_read_scroll", false)) + tmp.add(new Article(Article.TYPE_AMR_FOOTER)); - m_adapter.notifyDataSetChanged(); - m_listener.onHeadlinesLoaded(append); + m_adapter.submitList(tmp); + }); - } else { - m_lazyLoadDisabled = true; + return view; + } - if (m_lastError == ApiCommon.ApiError.LOGIN_FAILED) { - m_activity.login(true); - } else { - if (m_lastErrorMessage != null) { - m_activity.toast(getString(getErrorMessage()) + "\n" + m_lastErrorMessage); - } else { - m_activity.toast(getErrorMessage()); - } - } - } + @Override + public void onResume() { + super.onResume(); - // detail activity does not use footers (see above) - if (!(m_activity instanceof DetailActivity)) { - Application.getArticles().add(new Article(Article.TYPE_AMR_FOOTER)); - m_adapter.notifyDataSetChanged(); - } - } - }; - - final int skip = getSkip(append); - - final boolean allowForceUpdate = m_activity.getApiLevel() >= 9 && - !m_feed.is_cat && m_feed.id > 0 && !append && userInitiated && - skip == 0; - - Log.d(TAG, "allowForceUpdate=" + allowForceUpdate + " userInitiated=" + userInitiated + " skip=" + skip); - - req.setOffset(skip); - - HashMap<String,String> map = new HashMap<>(); - map.put("op", "getHeadlines"); - map.put("sid", sessionId); - map.put("feed_id", String.valueOf(m_feed.id)); - map.put("show_excerpt", "true"); - map.put("excerpt_length", String.valueOf(CommonActivity.EXCERPT_MAX_LENGTH)); - map.put("show_content", "true"); - map.put("include_attachments", "true"); - map.put("view_mode", m_activity.getViewMode()); - map.put("limit", m_prefs.getString("headlines_request_size", "15")); - map.put("offset", String.valueOf(0)); - map.put("skip", String.valueOf(skip)); - map.put("include_nested", "true"); - map.put("has_sandbox", "true"); - map.put("order_by", m_activity.getSortMode()); - - if (m_prefs.getBoolean("enable_image_downsampling", false)) { - if (m_prefs.getBoolean("always_downsample_images", false) || !m_activity.isWifiConnected()) { - map.put("resize_width", String.valueOf(m_activity.getResizeWidth())); - } - } + Log.d(TAG, "onResume"); - if (isCat) map.put("is_cat", "true"); + syncToSharedArticles(); - if (allowForceUpdate) { - map.put("force_update", "true"); - } + // we only set this in detail activity + if (m_activeArticleId > 0) { + Article activeArticle = Application.getArticles().getById(m_activeArticleId); - if (m_searchQuery != null && !m_searchQuery.isEmpty()) { - map.put("search", m_searchQuery); - map.put("search_mode", ""); - map.put("match_on", "both"); - } + if (activeArticle != null) + scrollToArticle(activeArticle); + } - if (m_firstId > 0) map.put("check_first_id", String.valueOf(m_firstId)); + m_activity.invalidateOptionsMenu(); + } - if (m_activity.getApiLevel() >= 12) { - map.put("include_header", "true"); - } + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); + m_activity = (OnlineActivity) activity; + m_listener = (HeadlinesEventListener) activity; + } - Log.d(TAG, "[HP] request more headlines, firstId=" + m_firstId); + public void refresh(final boolean append) { + ArticleModel model = Application.getArticlesModel(); - req.execute(map); - } - } + if (!append) + m_activeArticleId = -1; - private int getSkip(boolean append) { - int skip = 0; - - if (append) { - // adaptive, all_articles, marked, published, unread - String viewMode = m_activity.getViewMode(); - - int numUnread = Math.toIntExact(Application.getArticles().getUnreadCount()); - int numAll = Math.toIntExact(Application.getArticles().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; - } - } + if (m_swipeLayout != null) + m_swipeLayout.setRefreshing(true); - return skip; + model.setSearchQuery(getSearchQuery()); + model.startLoading(append, m_feed, m_activity.getResizeWidth()); } static class ArticleViewHolder extends RecyclerView.ViewHolder { public View view; - public Article article; public TextView titleView; public TextView feedTitleView; @@ -664,6 +578,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { public TextureView flavorVideoView; public MaterialButton attachmentsView; public ProgressTarget<String, GlideDrawable> flavorProgressTarget; + int articleId; public ArticleViewHolder(View v) { super(v); @@ -674,7 +589,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { View flavorImage = view.findViewById(R.id.flavor_image); if (flavorImage != null) { - article.flavorViewHeight = flavorImage.getMeasuredHeight(); + HeadlinesFragment.m_flavorHeightsCache.put(articleId, flavorImage.getMeasuredHeight()); } return true; @@ -705,6 +620,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (flavorImageView != null && flavorImageLoadingBar != null) { flavorProgressTarget = new FlavorProgressTarget<>(new GlideDrawableImageViewTarget(flavorImageView), flavorImageLoadingBar); } + } public void clearAnimation() { @@ -739,15 +655,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } } - private class ArticleListAdapter extends RecyclerView.Adapter<ArticleViewHolder> { - private final ArticleList items; - + private class ArticleListAdapter extends ListAdapter<Article, ArticleViewHolder> { public static final int VIEW_NORMAL = 0; public static final int VIEW_UNREAD = 1; public static final int VIEW_ACTIVE = 2; public static final int VIEW_ACTIVE_UNREAD = 3; - public static final int VIEW_LOADMORE = 4; - public static final int VIEW_AMR_FOOTER = 5; + public static final int VIEW_AMR_FOOTER = 4; public static final int VIEW_COUNT = VIEW_AMR_FOOTER + 1; @@ -779,9 +692,8 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { return false; } - public ArticleListAdapter(ArticleList items) { - super(); - this.items = items; + public ArticleListAdapter() { + super(new ArticleDiffItemCallback()); Display display = m_activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -803,9 +715,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { case VIEW_AMR_FOOTER: layoutId = R.layout.headlines_footer; break; - case VIEW_LOADMORE: - layoutId = R.layout.headlines_row_loadmore; - break; case VIEW_UNREAD: layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact_unread : R.layout.headlines_row_unread; break; @@ -826,12 +735,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { @Override public void onBindViewHolder(final ArticleViewHolder holder, int position) { - holder.article = items.get(position); - int headlineFontSize = m_prefs.getInt("headlines_font_size_sp_int", 13); int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2)); - final Article article = holder.article; + Article article = getItem(position); + + holder.articleId = article.id; if (article.id == Article.TYPE_AMR_FOOTER && m_prefs.getBoolean("headlines_mark_read_scroll", false)) { WindowManager wm = (WindowManager) m_activity.getSystemService(Context.WINDOW_SERVICE); @@ -854,9 +763,10 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { // only set active article when it makes sense (in DetailActivity) if (getActivity() instanceof DetailActivity) { - m_activeArticleId = article.id; - m_adapter.notifyDataSetChanged(); - } + m_activeArticleId = article.id; + + m_adapter.notifyItemChanged(position); + } }); // block footer clicks to make button/selection clicking easier @@ -867,18 +777,18 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } if (holder.textImage != null) { - updateTextCheckedState(holder, article); + updateTextCheckedState(holder, position); holder.textImage.setOnClickListener(view -> { - Log.d(TAG, "textImage : onclicked"); + Article selectedArticle = getItem(position); - article.selected = !article.selected; + Log.d(TAG, "textImage onClick pos=" + position + " article=" + article); - updateTextCheckedState(holder, article); + selectedArticle.selected = !selectedArticle.selected; - m_listener.onArticleListSelectionChange(getSelectedArticles()); + updateTextCheckedState(holder, position); - Log.d(TAG, "num selected: " + getSelectedArticles().size()); + m_listener.onArticleListSelectionChange(); }); ViewCompat.setTransitionName(holder.textImage, "gallery:" + article.flavorImageUri); @@ -931,11 +841,11 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { holder.markedView.setIconTint(ColorStateList.valueOf(tvPrimary.data)); holder.markedView.setOnClickListener(v -> { - article.marked = !article.marked; + Article selectedArticle = new Article(getItem(position)); + selectedArticle.marked = !selectedArticle.marked; - m_adapter.notifyItemChanged(m_list.getChildAdapterPosition(holder.view)); - - m_activity.saveArticleMarked(article); + m_activity.saveArticleMarked(selectedArticle); + Application.getArticlesModel().update(position, selectedArticle); }); } @@ -996,10 +906,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { holder.publishedView.setIconTint(ColorStateList.valueOf(tvPrimary.data)); holder.publishedView.setOnClickListener(v -> { - article.published = !article.published; - m_adapter.notifyItemChanged(m_list.getChildAdapterPosition(holder.view)); + Article selectedArticle = new Article(getItem(position)); + selectedArticle.published = !selectedArticle.published; + + m_activity.saveArticlePublished(selectedArticle); - m_activity.saveArticlePublished(article); + Application.getArticlesModel().update(position, selectedArticle); }); } @@ -1061,6 +973,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { holder.flavorVideoKindView.setVisibility(View.GONE); holder.flavorImageOverflow.setVisibility(View.GONE); holder.flavorVideoView.setVisibility(View.GONE); + holder.flavorImageHolder.setVisibility(View.GONE); Glide.clear(holder.flavorImageView); @@ -1124,10 +1037,17 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { holder.flavorImageView.setVisibility(View.VISIBLE); holder.flavorImageView.setMaxHeight((int)(m_screenHeight * 0.6f)); + // only show holder if we're about to display a picture + holder.flavorImageHolder.setVisibility(View.VISIBLE); + // prevent lower listiew entries from jumping around if this row is modified - if (article.flavorViewHeight > 0) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) holder.flavorImageView.getLayoutParams(); - lp.height = article.flavorViewHeight; + if (m_flavorHeightsCache.containsKey(article.id)) { + int cachedHeight = m_flavorHeightsCache.get(article.id); + + if (cachedHeight > 0) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) holder.flavorImageView.getLayoutParams(); + lp.height = cachedHeight; + } } holder.flavorProgressTarget.setModel(article.flavorImageUri); @@ -1320,18 +1240,19 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (holder.selectionBoxView != null) { holder.selectionBoxView.setChecked(article.selected); holder.selectionBoxView.setOnClickListener(view -> { - CheckBox cb = (CheckBox)view; + Article currentArticle = getItem(position); + + Log.d(TAG, "selectionCb onClick pos=" + position + " article=" + article); - article.selected = cb.isChecked(); + CheckBox cb = (CheckBox)view; - m_listener.onArticleListSelectionChange(getSelectedArticles()); + currentArticle.selected = cb.isChecked(); - Log.d(TAG, "num selected: " + getSelectedArticles().size()); + m_listener.onArticleListSelectionChange(); }); } if (holder.menuButtonView != null) { - holder.menuButtonView.setOnClickListener(v -> { PopupMenu popup = new PopupMenu(getActivity(), v); @@ -1341,7 +1262,8 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { popup.getMenu().findItem(R.id.article_set_labels).setEnabled(m_activity.getApiLevel() >= 1); popup.getMenu().findItem(R.id.article_edit_note).setEnabled(m_activity.getApiLevel() >= 1); - popup.setOnMenuItemClickListener(item -> onArticleMenuItemSelected(item, article, + popup.setOnMenuItemClickListener(item -> onArticleMenuItemSelected(item, + getItem(position), m_list.getChildAdapterPosition(holder.view))); popup.show(); @@ -1351,12 +1273,10 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { @Override public int getItemViewType(int position) { - Article a = items.get(position); + Article a = getItem(position); if (a.id == Article.TYPE_AMR_FOOTER) { return VIEW_AMR_FOOTER; - } else if (a.id == Article.TYPE_LOADMORE) { - return VIEW_LOADMORE; } else if (a.id == m_activeArticleId && a.unread) { return VIEW_ACTIVE_UNREAD; } else if (a.id == m_activeArticleId) { @@ -1368,12 +1288,9 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } } - @Override - public int getItemCount() { - return items.size(); - } + private void updateTextCheckedState(final ArticleViewHolder holder, int position) { + Article article = getItem(position); - private void updateTextCheckedState(final ArticleViewHolder holder, final Article article) { String tmp = !article.title.isEmpty() ? article.title.substring(0, 1).toUpperCase() : "?"; if (article.selected) { @@ -1508,47 +1425,60 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } } - public void notifyUpdated() { - m_adapter.notifyDataSetChanged(); - } - public void scrollToArticle(Article article) { scrollToArticleId(article.id); } public void scrollToArticleId(int id) { - m_list.scrollToPosition(Application.getArticles().getPositionById(id)); + int position = Application.getArticles().getPositionById(id); + + if (position != -1) + m_list.scrollToPosition(position); } public void setActiveArticleId(int articleId) { if (m_list != null && articleId != m_activeArticleId) { + ArticleList articles = Application.getArticles(); + + int oldPosition = articles.getPositionById(m_activeArticleId); + int newPosition = articles.getPositionById(articleId); + m_activeArticleId = articleId; - m_adapter.notifyDataSetChanged(); + + if (oldPosition != -1) + m_adapter.notifyItemChanged(oldPosition); + + m_adapter.notifyItemChanged(newPosition); scrollToArticleId(articleId); + + if (newPosition >= articles.size() - 5) + new Handler().postDelayed(() -> refresh(true), 0); } } public void setSelection(ArticlesSelection select) { - for (Article a : Application.getArticles()) - a.selected = false; + ArticleList articlesWithoutFooters = Application.getArticles().getWithoutFooters(); - if (select != ArticlesSelection.NONE) { - for (Article a : Application.getArticles()) { - if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) { - a.selected = true; - } - } - } + for (Article a : articlesWithoutFooters) { + if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) { + a.selected = true; - if (m_adapter != null) { - m_adapter.notifyDataSetChanged(); - } - } + int position = Application.getArticles().getPositionById(a.id); + + if (position != -1) + m_adapter.notifyItemChanged(position); + + } else if (a.selected) { + a.selected = false; + + int position = Application.getArticles().getPositionById(a.id); - public int getActiveArticleId() { - return m_activeArticleId; + if (position != -1) + m_adapter.notifyItemChanged(position); + } + } } public String getSearchQuery() { @@ -1559,10 +1489,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (!m_searchQuery.equals(query)) { m_searchQuery = query; - // could be called before fragment view has been initialized - if (m_list != null) { - refresh(false); - } + refresh(false); } } @@ -1577,4 +1504,15 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { releaseSurface(); } + private void syncToSharedArticles() { + ArticleList tmp = new ArticleList(); + + tmp.addAll(Application.getArticles()); + + if (m_prefs.getBoolean("headlines_mark_read_scroll", false)) + tmp.add(new Article(Article.TYPE_AMR_FOOTER)); + + m_adapter.submitList(tmp); + } + } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java index 9430ddc2..458f1f76 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java @@ -164,7 +164,7 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - if (m_prefs.getBoolean("enable_cats", false)) { + if (m_prefs.getBoolean("enable_cats", true)) { ft.replace(R.id.feeds_fragment, new FeedCategoriesFragment(), FRAG_CATS); } else { ft.replace(R.id.feeds_fragment, new FeedsFragment(), FRAG_FEEDS); @@ -445,7 +445,7 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList } @Override - public void onArticleListSelectionChange(ArticleList m_selectedArticles) { + public void onArticleListSelectionChange() { invalidateOptionsMenu(); } @@ -476,7 +476,7 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList // we use shared article list, but detail activity does not use special footers // we will append those back (if needed) in onActivityResult() - Application.getArticles().stripFooters(); + // Application.getArticles().stripFooters(); startActivityForResult(intent, HEADLINES_REQUEST); overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); @@ -520,19 +520,19 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == HEADLINES_REQUEST) { - - // we add back footers stripped when this was passed to DetailActivity - Application.getArticles().add(new Article(Article.TYPE_AMR_FOOTER)); + Log.d(TAG, "onActivityResult:" + requestCode + " "+ resultCode + " " + data); + if (requestCode == HEADLINES_REQUEST) { HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); if (hf != null) { - hf.notifyUpdated(); + // data might be null if detailactivity crashed + if (data != null) { + int activeArticleId = data.getIntExtra("activeArticleId", 0); - // this makes position in headlines in master activity (not quite) randomly jump around when returning - // even if active article hasn't been changed, i guess keeping it as-is is a lesser evil? - hf.scrollToArticleId(data.getIntExtra("activeArticleId", 0)); + Log.d(TAG, "got back from detail activity, scrolling to id=" + activeArticleId); + hf.scrollToArticleId(activeArticleId); + } } } } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java index 92a3f219..26dab50d 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java @@ -391,6 +391,7 @@ public class OnlineActivity extends CommonActivity { } else if (itemId == R.id.search) { if (hf != null) { final EditText edit = new EditText(this); + edit.setText(hf.getSearchQuery()); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this) .setTitle(R.string.search) @@ -561,28 +562,37 @@ public class OnlineActivity extends CommonActivity { if (ap != null) { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); - if (selectedArticle != null) - setArticleScore(selectedArticle); + if (selectedArticle != null) { + setArticleScore(selectedArticle); + + hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + } } return true; } else if (itemId == R.id.toggle_marked) { if (ap != null) { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); - selectedArticle.marked = !selectedArticle.marked; - saveArticleMarked(selectedArticle); + if (selectedArticle != null) { + selectedArticle.marked = !selectedArticle.marked; + + saveArticleMarked(selectedArticle); - if (hf != null) hf.notifyUpdated(); + hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + } } return true; } else if (itemId == R.id.toggle_unread) { if (ap != null) { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); - selectedArticle.unread = !selectedArticle.unread; - saveArticleUnread(selectedArticle); + if (selectedArticle != null) { + selectedArticle.unread = !selectedArticle.unread; + + saveArticleUnread(selectedArticle); - if (hf != null) hf.notifyUpdated(); + hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + } } return true; } else if (itemId == R.id.selection_toggle_unread) { @@ -590,11 +600,13 @@ public class OnlineActivity extends CommonActivity { ArticleList selected = hf.getSelectedArticles(); if (!selected.isEmpty()) { - for (Article a : selected) + for (Article a : selected) { a.unread = !a.unread; + hf.notifyItemChanged(Application.getArticles().indexOf(a)); + } + toggleArticlesUnread(selected); - hf.notifyUpdated(); invalidateOptionsMenu(); } } @@ -604,11 +616,13 @@ public class OnlineActivity extends CommonActivity { ArticleList selected = hf.getSelectedArticles(); if (!selected.isEmpty()) { - for (Article a : selected) + for (Article a : selected) { a.marked = !a.marked; + hf.notifyItemChanged(Application.getArticles().indexOf(a)); + } + toggleArticlesMarked(selected); - hf.notifyUpdated(); invalidateOptionsMenu(); } } @@ -618,24 +632,26 @@ public class OnlineActivity extends CommonActivity { ArticleList selected = hf.getSelectedArticles(); if (!selected.isEmpty()) { - for (Article a : selected) + for (Article a : selected) { a.published = !a.published; + hf.notifyItemChanged(Application.getArticles().indexOf(a)); + } + toggleArticlesPublished(selected); - hf.notifyUpdated(); invalidateOptionsMenu(); } } return true; } else if (itemId == R.id.toggle_published) { - if (ap != null) { + if (ap != null && hf != null) { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); if (selectedArticle != null) { selectedArticle.published = !selectedArticle.published; saveArticlePublished(selectedArticle); - if (hf != null) hf.notifyUpdated(); + hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); } } return true; @@ -688,12 +704,17 @@ public class OnlineActivity extends CommonActivity { if (a.unread) { a.unread = false; tmp.add(a); + + if (hf != null) { + int position = Application.getArticles().indexOf(a); + + hf.notifyItemChanged(position); + } } } if (!tmp.isEmpty()) { setArticlesUnread(tmp, Article.UPDATE_SET_FALSE); - hf.notifyUpdated(); invalidateOptionsMenu(); } } @@ -713,11 +734,15 @@ public class OnlineActivity extends CommonActivity { saveArticleNote(article, note); - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); - if (hf != null) hf.notifyUpdated(); + int position = Application.getArticles().getPositionById(article.id); - ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - if (ap != null) ap.notifyUpdated(); + if (position != -1) { + HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); + if (hf != null) hf.notifyItemChanged(position); + + ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); + if (ap != null) ap.notifyItemChanged(position); + } }); builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> { @@ -839,7 +864,8 @@ public class OnlineActivity extends CommonActivity { protected void setApiLevel(int apiLevel) { Application.getInstance().setApiLevel(apiLevel); } - + + // TODO switch to setArticleField() public void saveArticleUnread(final Article article) { ApiRequest req = new ApiRequest(getApplicationContext()) { protected void onPostExecute(JsonElement result) { @@ -853,11 +879,12 @@ public class OnlineActivity extends CommonActivity { map.put("op", "updateArticle"); map.put("article_ids", String.valueOf(article.id)); map.put("mode", article.unread ? "1" : "0"); - map.put("field", "2"); + map.put("field", String.valueOf(Article.UPDATE_FIELD_UNREAD)); req.execute(map); } + // TODO switch to setArticleField() public void saveArticleScore(final Article article) { ApiRequest req = new ApiRequest(getApplicationContext()) { protected void onPostExecute(JsonElement result) { @@ -871,11 +898,12 @@ public class OnlineActivity extends CommonActivity { map.put("op", "updateArticle"); map.put("article_ids", String.valueOf(article.id)); map.put("data", String.valueOf(article.score)); - map.put("field", "4"); + map.put("field", String.valueOf(Article.UPDATE_FIELD_SCORE)); req.execute(map); } + // TODO switch to setArticleField() public void saveArticleMarked(final Article article) { ApiRequest req = new ApiRequest(getApplicationContext()) { protected void onPostExecute(JsonElement result) { @@ -889,11 +917,12 @@ public class OnlineActivity extends CommonActivity { map.put("op", "updateArticle"); map.put("article_ids", String.valueOf(article.id)); map.put("mode", article.marked ? "1" : "0"); - map.put("field", "0"); + map.put("field", String.valueOf(Article.UPDATE_FIELD_MARKED)); req.execute(map); } + // TODO switch to setArticleField() public void saveArticlePublished(final Article article) { ApiRequest req = new ApiRequest(getApplicationContext()) { @@ -908,11 +937,12 @@ public class OnlineActivity extends CommonActivity { map.put("op", "updateArticle"); map.put("article_ids", String.valueOf(article.id)); map.put("mode", article.published ? "1" : "0"); - map.put("field", "1"); + map.put("field", String.valueOf(Article.UPDATE_FIELD_PUBLISHED)); req.execute(map); } + // TODO switch to setArticleField() public void saveArticleNote(final Article article, final String note) { ApiRequest req = new ApiRequest(getApplicationContext()) { protected void onPostExecute(JsonElement result) { @@ -926,7 +956,7 @@ public class OnlineActivity extends CommonActivity { map.put("article_ids", String.valueOf(article.id)); map.put("mode", "1"); map.put("data", note); - map.put("field", "3"); + map.put("field", String.valueOf(Article.UPDATE_FIELD_NOTE)); req.execute(map); } @@ -1005,7 +1035,8 @@ article.score = Integer.parseInt(edit.getText().toString()); if (selectedArticle != null) { selectedArticle.unread = !selectedArticle.unread; saveArticleUnread(selectedArticle); - if (hf != null) hf.notifyUpdated(); + + hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); } } return true; @@ -1081,8 +1112,6 @@ article.score = Integer.parseInt(edit.getText().toString()); } public void setArticlesUnread(final ArticleList articles, int mode) { - ApiRequest req = new ApiRequest(getApplicationContext()); - setArticleField(articles, Article.UPDATE_FIELD_UNREAD, mode); } @@ -1099,11 +1128,19 @@ article.score = Integer.parseInt(edit.getText().toString()); protected void onPostExecute(JsonElement result) { Log.d(TAG, "setArticleField operation complete"); - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); - if (hf != null) hf.notifyUpdated(); + // currently this is generally handled before operation completes (but after POJO is modified) + /* HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - if (ap != null) ap.notifyUpdated(); + + for (Article a : articles) { + int position = Application.getArticles().getPositionById(a.id); + + if (position != -1) { + if (hf != null) hf.notifyItemChanged(position); + if (ap != null) ap.notifyItemChanged(position); + } + } */ } }; @@ -1189,12 +1226,6 @@ article.score = Integer.parseInt(edit.getText().toString()); if (hf != null) { hf.refresh(false); } - - ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - - if (af != null) { - af.refresh(false); - } } } @@ -1363,15 +1394,6 @@ article.score = Integer.parseInt(edit.getText().toString()); return m_lastImageHitTestUrl; } - public boolean isWifiConnected() { - NetworkInfo wifi = m_cmgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - - if (wifi != null) - return wifi.isConnected(); - - return false; - } - public int getResizeWidth() { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java index 63458532..9c9187d9 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/CommonActivity.java @@ -10,7 +10,6 @@ public class CommonActivity extends Activity { private final String TAG = this.getClass().getSimpleName(); private boolean m_smallScreenMode = true; - private boolean m_compatMode = false; protected void setSmallScreen(boolean smallScreen) { Log.d(TAG, "m_smallScreenMode=" + smallScreen); @@ -27,23 +26,10 @@ public class CommonActivity extends Activity { toast.show(); } - @Override - public void onCreate(Bundle savedInstanceState) { - m_compatMode = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB; - - Log.d(TAG, "m_compatMode=" + m_compatMode); - - super.onCreate(savedInstanceState); - } - public boolean isSmallScreen() { return m_smallScreenMode; } - public boolean isCompatMode() { - return m_compatMode; - } - public boolean isPortrait() { Display display = getWindowManager().getDefaultDisplay(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java index 3a1553b6..66a77aef 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/ShareActivity.java @@ -86,7 +86,7 @@ public class ShareActivity extends CommonShareActivity { protected void onPostExecute(JsonElement result) { setProgressBarIndeterminateVisibility(false); - if (m_lastError != ApiCommon.ApiError.NO_ERROR) { + if (m_lastError != ApiCommon.ApiError.UNKNOWN_ERROR) { toast(getErrorMessage()); } else { toast(R.string.share_article_posted); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java index e71583d3..82e2d52d 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/share/SubscribeActivity.java @@ -157,7 +157,7 @@ public class SubscribeActivity extends CommonShareActivity { protected void onPostExecute(JsonElement result) { m_progressBar.setVisibility(View.INVISIBLE); - if (m_lastError != ApiCommon.ApiError.NO_ERROR) { + if (m_lastError != ApiCommon.ApiError.UNKNOWN_ERROR) { toast(getErrorMessage()); } else { try { @@ -259,7 +259,7 @@ public class SubscribeActivity extends CommonShareActivity { protected void onPostExecute(JsonElement result) { m_progressBar.setVisibility(View.INVISIBLE); - if (m_lastError != ApiCommon.ApiError.NO_ERROR) { + if (m_lastError != ApiCommon.ApiError.UNKNOWN_ERROR) { toast(getErrorMessage()); } else { JsonArray content = result.getAsJsonArray(); 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..f0795dd8 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 @@ -14,7 +14,6 @@ import java.util.regex.Pattern; // TODO: serialize Labels public class Article implements Parcelable { - public static final int TYPE_LOADMORE = -1; public static final int TYPE_AMR_FOOTER = -2; public static final int FLAVOR_KIND_ALBUM = 1; @@ -69,9 +68,8 @@ public class Article implements Parcelable { transient public String flavorStreamUri; transient public String youtubeVid; transient public List<Element> mediaList = new ArrayList<>(); - transient public int flavorViewHeight; - public Article(Parcel in) { + public Article(Parcel in) { readFromParcel(in); } @@ -194,8 +192,44 @@ public class Article implements Parcelable { public Article(int id) { this.id = id; this.title = "ID:" + id; - this.link = ""; - this.tags = new ArrayList<>(); + fixNullFields(); + } + + public Article(Article clone) { + id = clone.id; + unread = clone.unread; + marked = clone.marked; + published = clone.published; + score = clone.score; + updated = clone.updated; + is_updated = clone.is_updated; + title = clone.title; + link = clone.link; + feed_id = clone.feed_id; + tags = clone.tags; + attachments = clone.attachments; + content = clone.content; + excerpt = clone.excerpt; + labels = clone.labels; + feed_title = clone.feed_title; + comments_count = clone.comments_count; + comments_link = clone.comments_link; + always_display_attachments = clone.always_display_attachments; + author = clone.author; + note = clone.note; + selected = clone.selected; + flavor_image = clone.flavor_image; + flavor_stream = clone.flavor_stream; + flavor_kind = clone.flavor_kind; + site_url = clone.site_url; + + articleDoc = clone.articleDoc; + flavorImage = clone.flavorImage; + + flavorImageUri = clone.flavorImageUri; + flavorStreamUri = clone.flavorStreamUri; + youtubeVid = clone.youtubeVid; + mediaList = new ArrayList<>(clone.mediaList); } @Override @@ -262,13 +296,9 @@ public class Article implements Parcelable { } public boolean equalsById(Article article) { - if (article != null && id == article.id) { - return true; - } else { - return false; - } + return article != null && id == article.id; } - + @SuppressWarnings("rawtypes") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @@ -280,4 +310,14 @@ public class Article implements Parcelable { return new Article[size]; } }; + + /** set fields which might be missing during JSON deserialization to sane values */ + public void fixNullFields() { + if (note == null) note = ""; + if (link == null) link = ""; + if (tags == null) tags = new ArrayList<>(); + if (note == null) note = ""; + if (excerpt == null) excerpt = ""; + if (content == null) content = ""; + } } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java index 873b311b..6724acb0 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/types/ArticleList.java @@ -1,8 +1,5 @@ package org.fox.ttrss.types; -import android.os.Parcel; -import android.os.Parcelable; - import java.util.ListIterator; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; @@ -26,6 +23,10 @@ public class ArticleList extends CopyOnWriteArrayList<Article> { public ArticleList() { } + public ArticleList(ArticleList clone) { + this.addAll(clone); + } + public ArticleList getWithoutFooters() { return this.stream().filter(a -> { return a.id > 0; }).collect(Collectors.toCollection(ArticleList::new)); } @@ -34,22 +35,6 @@ public class ArticleList extends CopyOnWriteArrayList<Article> { return this.stream().filter(a -> { return a.unread; }).count(); } - public long getSizeWithoutFooters() { - return this.stream().filter(a -> { return a.id > 0; }).count(); - } - - /** strips all trailing items with negative IDs (Article.TYPE_LOADMORE, Article.TYPE_AMR_FOOTER) */ - public void stripFooters() { - for (ListIterator<Article> iterator = this.listIterator(size()); iterator.hasPrevious();) { - final Article article = iterator.previous(); - - if (article.id < 0) - this.remove(article); - else - break; - } - } - public int getPositionById(int id) { for (int i = 0; i < size(); i++) { if (get(i).id == id) { diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java new file mode 100644 index 00000000..b037eea0 --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java @@ -0,0 +1,21 @@ +package org.fox.ttrss.util; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import org.fox.ttrss.types.Article; + +public class ArticleDiffItemCallback extends DiffUtil.ItemCallback<Article> { + private final String TAG = this.getClass().getSimpleName(); + @Override + public boolean areItemsTheSame(@NonNull Article a1, @NonNull Article a2) { + return a1.id == a2.id; + } + + @Override + public boolean areContentsTheSame(@NonNull Article a1, @NonNull Article a2) { + return a1.id == a2.id && a1.unread == a2.unread && a1.marked == a2.marked + && a1.selected == a2.selected && a1.published == a2.published + && a1.note.equals(a2.note); + } +} diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/DiffFragmentStateAdapter.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/DiffFragmentStateAdapter.java new file mode 100644 index 00000000..c423b02d --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/DiffFragmentStateAdapter.java @@ -0,0 +1,54 @@ +package org.fox.ttrss.util; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; +import androidx.recyclerview.widget.AsyncListDiffer; +import androidx.recyclerview.widget.DiffUtil; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import java.util.List; + +// https://gist.github.com/Gnzlt/7e8a23ba0c3b046ed33c824b284d7270 +public abstract class DiffFragmentStateAdapter<T> extends FragmentStateAdapter { + + private final AsyncListDiffer<T> differ; + + protected DiffFragmentStateAdapter(FragmentActivity fragmentActivity, DiffUtil.ItemCallback<T> diffCallback) { + super(fragmentActivity); + differ = new AsyncListDiffer<>(this, diffCallback); + } + + protected DiffFragmentStateAdapter(Fragment fragment, DiffUtil.ItemCallback<T> diffCallback) { + super(fragment); + differ = new AsyncListDiffer<>(this, diffCallback); + } + + protected DiffFragmentStateAdapter(FragmentManager fragmentManager, Lifecycle lifecycle, DiffUtil.ItemCallback<T> diffCallback) { + super(fragmentManager, lifecycle); + differ = new AsyncListDiffer<>(this, diffCallback); + } + + public void submitList(List<T> list, Runnable commitCallback) { + differ.submitList(list, commitCallback); + } + + public void submitList(List<T> list) { + differ.submitList(list, null); + } + + public List<T> getCurrentList() { + return differ.getCurrentList(); + } + + protected T getItem(int position) { + return differ.getCurrentList().get(position); + } + + @Override + public int getItemCount() { + return differ.getCurrentList().size(); + } +} + diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java deleted file mode 100755 index 82698ffb..00000000 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/HeadlinesRequest.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.fox.ttrss.util; - -import android.content.Context; -import android.util.Log; - -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; -import org.fox.ttrss.ApiRequest; -import org.fox.ttrss.Application; -import org.fox.ttrss.OnlineActivity; -import org.fox.ttrss.types.Article; -import org.fox.ttrss.types.ArticleList; - -import java.lang.reflect.Type; -import java.util.List; - -public class HeadlinesRequest extends ApiRequest { - private final String TAG = this.getClass().getSimpleName(); - - private int m_offset = 0; - private final OnlineActivity m_activity; - protected boolean m_firstIdChanged = false; - protected int m_firstId = 0; - protected int m_amountLoaded = 0; - - public HeadlinesRequest(Context context, OnlineActivity activity, ArticleList articles) { - super(context); - - m_activity = activity; - } - - protected void onPostExecute(JsonElement result) { - if (result != null) { - try { - JsonArray content = result.getAsJsonArray(); - if (content != null) { - final List<Article> articlesJson; - final JsonObject header; - - if (m_activity.getApiLevel() >= 12) { - header = content.get(0).getAsJsonObject(); - - //Log.d(TAG, "headerID:" + header.get("top_id_changed")); - - 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); - } - - ArticleList articles = Application.getArticles(); - - if (m_offset == 0) - articles.clear(); - else - articles.stripFooters(); - - m_amountLoaded = articlesJson.size(); - - for (Article f : articlesJson) - if (!articles.containsId(f.id)) { - f.collectMediaInfo(); - f.cleanupExcerpt(); - articles.add(f); - } - - return; - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - 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); - } - } - - public void setOffset(int skip) { - m_offset = skip; - } -} diff --git a/org.fox.ttrss/src/main/res/drawable/ic_launcher_background_variant.xml b/org.fox.ttrss/src/main/res/drawable/ic_launcher_background_variant.xml new file mode 100644 index 00000000..749ffa26 --- /dev/null +++ b/org.fox.ttrss/src/main/res/drawable/ic_launcher_background_variant.xml @@ -0,0 +1,42 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="51.06383" + android:viewportHeight="51.06383"> + <group android:translateX="-10.468085" + android:translateY="-10.468085"> + <path + android:pathData="M-9.8995,-7.7102h94.3666v92.4381h-94.3666z" + android:fillColor="#5c5c5c" + android:strokeColor="#00000000" + android:fillAlpha="1"/> + <path + android:pathData="M5.668,58.0128a8.3196,8.3198 0,1 0,16.6393 0a8.3196,8.3198 0,1 0,-16.6393 0z" + android:strokeAlpha="1" + android:strokeWidth="0.69765961" + android:fillColor="#808080" + android:strokeColor="#00000000" + android:fillAlpha="1"/> + <path + android:pathData="m45.8796,66.3326 l-11.7862,0A28.4254,28.426 0,0 0,5.668 37.9066l0,-11.7864A40.2116,40.2124 0,0 1,45.8796 66.3326Z" + android:strokeAlpha="1" + android:strokeWidth="0.69765961" + android:fillColor="#808080" + android:strokeColor="#00000000" + android:fillAlpha="1"/> + <path + android:pathData="M54.1992,66.3326A48.5312,48.5322 0,0 0,5.668 17.8004l0,-12.1331a60.6653,60.6641 90,0 1,60.6641 60.6653z" + android:strokeAlpha="1" + android:strokeWidth="2.01254654" + android:fillColor="#808080" + android:strokeColor="#00000000" + android:fillAlpha="1"/> + <path + android:pathData="M54.1992,66.3326A48.5312,48.5322 0,0 0,5.668 17.8004l0,-12.1331a60.6653,60.6641 90,0 1,60.6641 60.6653z" + android:strokeAlpha="1" + android:strokeWidth="2.01254654" + android:fillColor="#808080" + android:strokeColor="#00000000" + android:fillAlpha="1"/> + </group> +</vector> diff --git a/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_detail.xml b/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_detail.xml index 59c1b086..823afb9b 100644 --- a/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_detail.xml +++ b/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_detail.xml @@ -54,7 +54,7 @@ <FrameLayout android:layout_width="match_parent" android:id="@+id/article_fragment" - app:layout_behavior=".util.DetailActivityScrollingViewBehavior" + app:layout_behavior="org.fox.ttrss.util.DetailActivityScrollingViewBehavior" android:layout_height="match_parent"/> <com.google.android.material.bottomappbar.BottomAppBar diff --git a/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_master.xml b/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_master.xml index b6a6f19d..63b8c3e2 100644 --- a/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_master.xml +++ b/org.fox.ttrss/src/main/res/layout-sw600dp-land/activity_master.xml @@ -61,7 +61,7 @@ android:id="@+id/headlines_fragment" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_behavior=".util.FabAwareScrollingViewBehavior" /> + app:layout_behavior="org.fox.ttrss.util.FabAwareScrollingViewBehavior" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/master_fab" diff --git a/org.fox.ttrss/src/main/res/layout/fragment_headlines.xml b/org.fox.ttrss/src/main/res/layout/fragment_headlines.xml index 2783a1e4..647c3ae9 100755 --- a/org.fox.ttrss/src/main/res/layout/fragment_headlines.xml +++ b/org.fox.ttrss/src/main/res/layout/fragment_headlines.xml @@ -12,7 +12,6 @@ <org.fox.ttrss.util.ContextMenuRecyclerView android:id="@+id/headlines_list" android:background="?colorSurfaceContainer" - android:drawSelectorOnTop="true" android:scrollbars="vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml b/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml index 5721dcb8..f6483350 100755 --- a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml +++ b/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml @@ -101,6 +101,7 @@ style="?attr/materialIconButtonStyle" android:layout_width="wrap_content" android:paddingEnd="0dp" + android:layout_gravity="end" android:layout_height="24dp" android:layout_weight="0.5" app:icon="?ic_star_outline" /> diff --git a/org.fox.ttrss/src/main/res/layout/layout_detail_phone.xml b/org.fox.ttrss/src/main/res/layout/layout_detail_phone.xml index 55a6ba77..4e75d44b 100644 --- a/org.fox.ttrss/src/main/res/layout/layout_detail_phone.xml +++ b/org.fox.ttrss/src/main/res/layout/layout_detail_phone.xml @@ -14,7 +14,7 @@ <FrameLayout android:id="@+id/article_fragment" - app:layout_behavior=".util.DetailActivityScrollingViewBehavior" + app:layout_behavior="org.fox.ttrss.util.DetailActivityScrollingViewBehavior" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> diff --git a/org.fox.ttrss/src/main/res/layout/layout_master_phone.xml b/org.fox.ttrss/src/main/res/layout/layout_master_phone.xml index b4cb6b59..e32d5fbe 100644 --- a/org.fox.ttrss/src/main/res/layout/layout_master_phone.xml +++ b/org.fox.ttrss/src/main/res/layout/layout_master_phone.xml @@ -29,7 +29,7 @@ <FrameLayout android:id="@+id/headlines_fragment" - app:layout_behavior=".util.FabAwareScrollingViewBehavior" + app:layout_behavior="org.fox.ttrss.util.FabAwareScrollingViewBehavior" android:layout_width="match_parent" android:layout_height="wrap_content"/> diff --git a/org.fox.ttrss/src/main/res/mipmap-anydpi-v26/ic_launcher_variant.xml b/org.fox.ttrss/src/main/res/mipmap-anydpi-v26/ic_launcher_variant.xml new file mode 100644 index 00000000..d49922f9 --- /dev/null +++ b/org.fox.ttrss/src/main/res/mipmap-anydpi-v26/ic_launcher_variant.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background_variant"/> + <foreground android:drawable="@drawable/ic_launcher_foreground"/> +</adaptive-icon>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/values/strings.xml b/org.fox.ttrss/src/main/res/values/strings.xml index 6a67013a..fb7e6309 100755 --- a/org.fox.ttrss/src/main/res/values/strings.xml +++ b/org.fox.ttrss/src/main/res/values/strings.xml @@ -303,4 +303,5 @@ <string name="check_for_updates">Check for updates</string> <string name="dont_open_anything">Nothing</string> <string name="open_on_startup">Open on startup</string> + <string name="error_success">Operation completed successfully</string> </resources> diff --git a/org.fox.ttrss/src/main/res/xml/preferences.xml b/org.fox.ttrss/src/main/res/xml/preferences.xml index 81fbd5f1..8d064587 100755 --- a/org.fox.ttrss/src/main/res/xml/preferences.xml +++ b/org.fox.ttrss/src/main/res/xml/preferences.xml @@ -44,7 +44,7 @@ android:title="@string/sort_feeds_by_unread" /> <SwitchPreferenceCompat - android:defaultValue="false" + android:defaultValue="true" android:key="enable_cats" android:title="@string/enable_cats" /> @@ -108,7 +108,6 @@ <SwitchPreferenceCompat android:defaultValue="true" - android:dependency="headlines_mark_read_scroll" android:key="headlines_swipe_to_dismiss" android:summary="@string/pref_headlines_swipe_to_dismiss_long" android:title="@string/pref_headlines_swipe_to_dismiss" /> |