New preference to load next feed on mark read (#15)

New preference to load next feed on mark read. When the option is enabled, a session data source is created to calculate the next feed or folder to load.
This commit is contained in:
sictiru 2022-10-25 21:09:45 -07:00 committed by GitHub
parent b88906404c
commit 48dad79bab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 557 additions and 78 deletions

View file

@ -44,6 +44,8 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "com.google.dagger:hilt-android:2.43.2" implementation "com.google.dagger:hilt-android:2.43.2"
kapt "com.google.dagger:hilt-compiler:2.43.2" kapt "com.google.dagger:hilt-compiler:2.43.2"
testImplementation "junit:junit:4.13.2"
} }
android { android {
@ -71,6 +73,9 @@ android {
res.srcDirs = ['res'] res.srcDirs = ['res']
assets.srcDirs = ['assets'] assets.srcDirs = ['assets']
} }
test {
java.srcDirs = ['test']
}
} }
buildTypes { buildTypes {

View file

@ -579,6 +579,8 @@
<item>DOWN_NEXT</item> <item>DOWN_NEXT</item>
</string-array> </string-array>
<string name="default_volume_key_navigation_value">OFF</string> <string name="default_volume_key_navigation_value">OFF</string>
<string name="settings_load_next_on_mark_read">Open next feed/folder after read</string>
<string name="settings_load_next_on_mark_read_summary">Load the next feed/folder after marked as read</string>
<string name="settings_confirm_mark_all_read">Confirm mark all read on…</string> <string name="settings_confirm_mark_all_read">Confirm mark all read on…</string>
<string name="none">Neither</string> <string name="none">Neither</string>

View file

@ -148,6 +148,11 @@
android:entries="@array/volume_key_navigation_entries" android:entries="@array/volume_key_navigation_entries"
android:entryValues="@array/volume_key_navigation_values" android:entryValues="@array/volume_key_navigation_values"
android:defaultValue="@string/default_volume_key_navigation_value" /> android:defaultValue="@string/default_volume_key_navigation_value" />
<CheckBoxPreference
android:defaultValue="false"
android:key="load_next_on_mark_read"
android:title="@string/settings_load_next_on_mark_read"
android:summary="@string/settings_load_next_on_mark_read_summary" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View file

@ -3,6 +3,9 @@ package com.newsblur.activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -12,17 +15,30 @@ import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory; import com.google.android.play.core.review.ReviewManagerFactory;
import com.google.android.play.core.tasks.Task; import com.google.android.play.core.tasks.Task;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.di.IconLoader;
import com.newsblur.domain.Feed; import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment; import com.newsblur.fragment.DeleteFeedFragment;
import com.newsblur.fragment.FeedIntelTrainerFragment; import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameDialogFragment; import com.newsblur.fragment.RenameDialogFragment;
import com.newsblur.util.FeedExt; import com.newsblur.util.FeedExt;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.Session;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class FeedItemsList extends ItemsList { public class FeedItemsList extends ItemsList {
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_FEED = "feed"; public static final String EXTRA_FEED = "feed";
public static final String EXTRA_FOLDER_NAME = "folderName"; public static final String EXTRA_FOLDER_NAME = "folderName";
private Feed feed; private Feed feed;
@ -31,22 +47,21 @@ public class FeedItemsList extends ItemsList {
private ReviewInfo reviewInfo; private ReviewInfo reviewInfo;
public static void startActivity(Context context, FeedSet feedSet, public static void startActivity(Context context, FeedSet feedSet,
Feed feed, String folderName) { Feed feed, String folderName,
@Nullable SessionDataSource sessionDataSource) {
Intent intent = new Intent(context, FeedItemsList.class); Intent intent = new Intent(context, FeedItemsList.class);
intent.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
intent.putExtra(FeedItemsList.EXTRA_FEED, feed); intent.putExtra(FeedItemsList.EXTRA_FEED, feed);
intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName); intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName);
intent.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
intent.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
context.startActivity(intent); context.startActivity(intent);
} }
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
super.onCreate(bundle); super.onCreate(bundle);
setupFeedItems(getIntent());
UIUtils.setupToolbar(this, feed.faviconUrl, feed.title, iconLoader, false); viewModel.getNextSession().observe(this, this::setupFeedItems);
checkInAppReview(); checkInAppReview();
} }
@ -138,6 +153,28 @@ public class FeedItemsList extends ItemsList {
return "feed:" + feed.feedId; return "feed:" + feed.feedId;
} }
private void setupFeedItems(Session session) {
Feed feed = session.getFeed();
String folderName = session.getFolderName();
if (feed != null && folderName != null) {
setupFeedItems(feed, folderName);
} else {
finish();
}
}
private void setupFeedItems(Intent intent) {
Feed feed = (Feed) intent.getSerializableExtra(EXTRA_FEED);
String folderName = intent.getStringExtra(EXTRA_FOLDER_NAME);
setupFeedItems(feed, folderName);
}
private void setupFeedItems(@NonNull Feed feed, @NonNull String folderName) {
this.feed = feed;
this.folderName = folderName;
UIUtils.setupToolbar(this, feed.faviconUrl, feed.title, iconLoader, false);
}
private void checkInAppReview() { private void checkInAppReview() {
if (!PrefsUtils.hasInAppReviewed(this)) { if (!PrefsUtils.hasInAppReviewed(this)) {
reviewManager = ReviewManagerFactory.create(this); reviewManager = ReviewManagerFactory.create(this);

View file

@ -12,15 +12,19 @@ public class FolderItemsList extends ItemsList {
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
super.onCreate(bundle); super.onCreate(bundle);
setupFolder(getIntent().getStringExtra(EXTRA_FOLDER_NAME));
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false); viewModel.getNextSession().observe(this, session ->
setupFolder(session.getFolderName()));
} }
@Override @Override
String getSaveSearchFeedId() { String getSaveSearchFeedId() {
return "river:" + folderName; return "river:" + folderName;
} }
private void setupFolder(String folderName) {
this.folderName = folderName;
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false);
}
} }

View file

@ -5,8 +5,12 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY; import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
@ -19,23 +23,25 @@ import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.databinding.ActivityItemslistBinding; import com.newsblur.databinding.ActivityItemslistBinding;
import com.newsblur.delegate.ItemListContextMenuDelegate; import com.newsblur.delegate.ItemListContextMenuDelegate;
import com.newsblur.delegate.ItemListContextMenuDelegateImpl; import com.newsblur.delegate.ItemListContextMenuDelegateImpl;
import com.newsblur.di.IconLoader;
import com.newsblur.fragment.ItemSetFragment; import com.newsblur.fragment.ItemSetFragment;
import com.newsblur.service.NBSyncService; import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageLoader; import com.newsblur.util.ReadingActionListener;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.Session;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.StateFilter; import com.newsblur.util.StateFilter;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;
import com.newsblur.viewModel.ItemListViewModel;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public abstract class ItemsList extends NbActivity { public abstract class ItemsList extends NbActivity implements ReadingActionListener {
@Inject @Inject
BlurDatabaseHelper dbHelper; BlurDatabaseHelper dbHelper;
@ -43,22 +49,22 @@ public abstract class ItemsList extends NbActivity {
@Inject @Inject
FeedUtils feedUtils; FeedUtils feedUtils;
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_FEED_SET = "feed_set"; public static final String EXTRA_FEED_SET = "feed_set";
public static final String EXTRA_STORY_HASH = "story_hash"; public static final String EXTRA_STORY_HASH = "story_hash";
public static final String EXTRA_WIDGET_STORY = "widget_story"; public static final String EXTRA_WIDGET_STORY = "widget_story";
public static final String EXTRA_VISIBLE_SEARCH = "visibleSearch"; public static final String EXTRA_VISIBLE_SEARCH = "visibleSearch";
public static final String EXTRA_SESSION_DATA = "session_data";
private static final String BUNDLE_ACTIVE_SEARCH_QUERY = "activeSearchQuery"; private static final String BUNDLE_ACTIVE_SEARCH_QUERY = "activeSearchQuery";
private ActivityItemslistBinding binding;
protected ItemListContextMenuDelegate contextMenuDelegate; protected ItemListViewModel viewModel;
protected ItemSetFragment itemSetFragment;
protected StateFilter intelState;
protected FeedSet fs; protected FeedSet fs;
private ItemSetFragment itemSetFragment;
private ActivityItemslistBinding binding;
private ItemListContextMenuDelegate contextMenuDelegate;
@Nullable
private SessionDataSource sessionDataSource;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
@ -66,8 +72,9 @@ public abstract class ItemsList extends NbActivity {
overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left); overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left);
contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils); contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
fs = (FeedSet) getIntent().getSerializableExtra(EXTRA_FEED_SET); fs = (FeedSet) getIntent().getSerializableExtra(EXTRA_FEED_SET);
intelState = PrefsUtils.getStateFilter(this); sessionDataSource = (SessionDataSource) getIntent().getSerializableExtra(EXTRA_SESSION_DATA);
// this is not strictly necessary, since our first refresh with the fs will swap in // this is not strictly necessary, since our first refresh with the fs will swap in
// the correct session, but that can be delayed by sync backup, so we try here to // the correct session, but that can be delayed by sync backup, so we try here to
@ -77,6 +84,7 @@ public abstract class ItemsList extends NbActivity {
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH); String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
UIUtils.startReadingActivity(fs, hash, this); UIUtils.startReadingActivity(fs, hash, this);
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) { } else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
StateFilter intelState = PrefsUtils.getStateFilter(this);
if (dbHelper.getUnreadCount(fs, intelState) > 0) { if (dbHelper.getUnreadCount(fs, intelState) > 0) {
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this); UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
} }
@ -188,6 +196,27 @@ public abstract class ItemsList extends NbActivity {
} }
} }
@Override
public void onReadingActionCompleted() {
if (sessionDataSource != null) {
Session session = sessionDataSource.getNextSession();
if (session != null) {
// set the next session on the parent activity
fs = session.getFeedSet();
feedUtils.prepareReadingSession(fs, false);
triggerSync();
// set the next session on the child activity
viewModel.updateSession(session);
// update item set fragment
itemSetFragment.resetEmptyState();
itemSetFragment.hasUpdated();
itemSetFragment.scrollToTop();
} else finish();
} else finish();
}
private void updateStatusIndicators() { private void updateStatusIndicators() {
if (binding.itemlistSyncStatus != null) { if (binding.itemlistSyncStatus != null) {
String syncStatus = NBSyncService.getSyncStatusMessage(this, true); String syncStatus = NBSyncService.getSyncStatusMessage(this, true);
@ -266,4 +295,5 @@ public abstract class ItemsList extends NbActivity {
} }
abstract String getSaveSearchFeedId(); abstract String getSaveSearchFeedId();
} }

View file

@ -2,11 +2,22 @@ package com.newsblur.activity;
import android.os.Bundle; import android.os.Bundle;
import com.newsblur.di.IconLoader;
import com.newsblur.domain.SocialFeed; import com.newsblur.domain.SocialFeed;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class SocialFeedItemsList extends ItemsList { public class SocialFeedItemsList extends ItemsList {
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_SOCIAL_FEED = "social_feed"; public static final String EXTRA_SOCIAL_FEED = "social_feed";
private SocialFeed socialFeed; private SocialFeed socialFeed;

View file

@ -1,5 +1,13 @@
package com.newsblur.database; package com.newsblur.database;
import static com.newsblur.util.AppConstants.ALL_SHARED_STORIES_GROUP_KEY;
import static com.newsblur.util.AppConstants.ALL_STORIES_GROUP_KEY;
import static com.newsblur.util.AppConstants.GLOBAL_SHARED_STORIES_GROUP_KEY;
import static com.newsblur.util.AppConstants.INFREQUENT_SITE_STORIES_GROUP_KEY;
import static com.newsblur.util.AppConstants.READ_STORIES_GROUP_KEY;
import static com.newsblur.util.AppConstants.SAVED_SEARCHES_GROUP_KEY;
import static com.newsblur.util.AppConstants.SAVED_STORIES_GROUP_KEY;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -35,8 +43,10 @@ import com.newsblur.domain.Folder;
import com.newsblur.domain.SavedSearch; import com.newsblur.domain.SavedSearch;
import com.newsblur.domain.StarredCount; import com.newsblur.domain.StarredCount;
import com.newsblur.domain.SocialFeed; import com.newsblur.domain.SocialFeed;
import com.newsblur.util.Session;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedListOrder; import com.newsblur.util.FeedListOrder;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.SpacingStyle; import com.newsblur.util.SpacingStyle;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.ImageLoader; import com.newsblur.util.ImageLoader;
@ -52,21 +62,6 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
private enum GroupType { GLOBAL_SHARED_STORIES, ALL_SHARED_STORIES, INFREQUENT_STORIES, ALL_STORIES, FOLDER, READ_STORIES, SAVED_SEARCHES, SAVED_STORIES } private enum GroupType { GLOBAL_SHARED_STORIES, ALL_SHARED_STORIES, INFREQUENT_STORIES, ALL_STORIES, FOLDER, READ_STORIES, SAVED_SEARCHES, SAVED_STORIES }
private enum ChildType { SOCIAL_FEED, FEED, SAVED_BY_TAG, SAVED_SEARCH } private enum ChildType { SOCIAL_FEED, FEED, SAVED_BY_TAG, SAVED_SEARCH }
// The following keys are used to mark the position of the special meta-folders within
// the folders array. Since the ExpandableListView doesn't handle collapsing of views
// set to View.GONE, we have to totally remove any hidden groups from the group count
// and adjust all folder indicies accordingly. Fake folders are created with these
// very unlikely names and layout methods check against them before assuming a row is
// a normal folder. All the string comparison is a small price to pay to avoid the
// alternative of index-counting in a situation where some rows might be disabled.
private static final String GLOBAL_SHARED_STORIES_GROUP_KEY = "GLOBAL_SHARED_STORIES_GROUP_KEY";
private static final String ALL_SHARED_STORIES_GROUP_KEY = "ALL_SHARED_STORIES_GROUP_KEY";
private static final String ALL_STORIES_GROUP_KEY = "ALL_STORIES_GROUP_KEY";
private static final String INFREQUENT_SITE_STORIES_GROUP_KEY = "INFREQUENT_SITE_STORIES_GROUP_KEY";
private static final String READ_STORIES_GROUP_KEY = "READ_STORIES_GROUP_KEY";
private static final String SAVED_STORIES_GROUP_KEY = "SAVED_STORIES_GROUP_KEY";
private static final String SAVED_SEARCHES_GROUP_KEY = "SAVED_SEARCHES_GROUP_KEY";
private final static float defaultTextSize_childName = 14; private final static float defaultTextSize_childName = 14;
private final static float defaultTextSize_groupName = 13; private final static float defaultTextSize_groupName = 13;
private final static float defaultTextSize_count = 13; private final static float defaultTextSize_count = 13;
@ -982,4 +977,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
this.spacingStyle = spacingStyle; this.spacingStyle = spacingStyle;
} }
public SessionDataSource buildSessionDataSource(Session activeSession) {
return new SessionDataSource(activeSession, activeFolderNames, activeFolderChildren);
}
} }

View file

@ -423,11 +423,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return true; return true;
case R.id.menu_mark_older_stories_as_read: case R.id.menu_mark_older_stories_as_read:
feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options, false); feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options);
return true; return true;
case R.id.menu_mark_newer_stories_as_read: case R.id.menu_mark_newer_stories_as_read:
feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options, false); feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options);
return true; return true;
case R.id.menu_send_story: case R.id.menu_send_story:
@ -456,7 +456,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
case R.id.menu_go_to_feed: case R.id.menu_go_to_feed:
FeedSet fs = FeedSet.singleFeed(story.feedId); FeedSet fs = FeedSet.singleFeed(story.feedId);
FeedItemsList.startActivity(context, fs, FeedItemsList.startActivity(context, fs,
feedUtils.getFeed(story.feedId), null); feedUtils.getFeed(story.feedId), null, null);
return true; return true;
default: default:
return false; return false;

View file

@ -27,7 +27,7 @@ interface ItemListContextMenuDelegate {
open class ItemListContextMenuDelegateImpl( open class ItemListContextMenuDelegateImpl(
private val activity: ItemsList, private val activity: ItemsList,
private val feedUtils: FeedUtils, private val feedUtils: FeedUtils,
) : ItemListContextMenuDelegate { ) : ItemListContextMenuDelegate, ReadingActionListener by activity {
override fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean { override fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean {
menuInflater.inflate(R.menu.itemslist, menu) menuInflater.inflate(R.menu.itemslist, menu)
@ -171,7 +171,7 @@ open class ItemListContextMenuDelegateImpl(
activity.finish() activity.finish()
return true return true
} else if (item.itemId == R.id.menu_mark_all_as_read) { } else if (item.itemId == R.id.menu_mark_all_as_read) {
feedUtils.markRead(activity, fs, null, null, R.array.mark_all_read_options, true) feedUtils.markRead(activity, fs, null, null, R.array.mark_all_read_options, this)
return true return true
} else if (item.itemId == R.id.menu_story_order_newest) { } else if (item.itemId == R.id.menu_story_order_newest) {
updateStoryOrder(fragment, fs, StoryOrder.NEWEST) updateStoryOrder(fragment, fs, StoryOrder.NEWEST)

View file

@ -10,10 +10,6 @@ annotation class StoryFileCache
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
annotation class IconFileCache annotation class IconFileCache
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ThumbnailFileCache
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
annotation class IconLoader annotation class IconLoader

View file

@ -12,6 +12,7 @@ import java.util.List;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import com.newsblur.database.DatabaseConstants; import com.newsblur.database.DatabaseConstants;
import com.newsblur.util.FeedListOrder; import com.newsblur.util.FeedListOrder;
import com.newsblur.util.FeedUtils;
public class Feed implements Comparable<Feed>, Serializable { public class Feed implements Comparable<Feed>, Serializable {
@ -153,7 +154,7 @@ public class Feed implements Comparable<Feed>, Serializable {
public boolean equals(Object o) { public boolean equals(Object o) {
if (! (o instanceof Feed)) return false; if (! (o instanceof Feed)) return false;
Feed otherFeed = (Feed) o; Feed otherFeed = (Feed) o;
return (TextUtils.equals(feedId, otherFeed.feedId)); return (FeedUtils.textUtilsEquals(feedId, otherFeed.feedId));
} }
@Override @Override

View file

@ -51,8 +51,10 @@ import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder; import com.newsblur.domain.Folder;
import com.newsblur.domain.SavedSearch; import com.newsblur.domain.SavedSearch;
import com.newsblur.domain.SocialFeed; import com.newsblur.domain.SocialFeed;
import com.newsblur.util.Session;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedExt; import com.newsblur.util.FeedExt;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.SpacingStyle; import com.newsblur.util.SpacingStyle;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
@ -392,7 +394,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
} }
private void markFeedsAsRead(FeedSet fs) { private void markFeedsAsRead(FeedSet fs) {
feedUtils.markRead(((NbActivity) getActivity()), fs, null, null, R.array.mark_all_read_options, false); feedUtils.markRead(((NbActivity) getActivity()), fs, null, null, R.array.mark_all_read_options);
adapter.lastFeedViewedId = fs.getSingleFeed(); adapter.lastFeedViewedId = fs.getSingleFeed();
adapter.lastFolderViewed = fs.getFolderName(); adapter.lastFolderViewed = fs.getFolderName();
} }
@ -437,7 +439,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
@Override @Override
public boolean onGroupClick(ExpandableListView list, View group, int groupPosition, long id) { public boolean onGroupClick(ExpandableListView list, View group, int groupPosition, long id) {
Intent i = null; if (adapter.isRowSavedSearches(groupPosition)) {
// group not clickable
return true;
}
FeedSet fs = adapter.getGroup(groupPosition);
Intent i;
if (adapter.isRowAllStories(groupPosition)) { if (adapter.isRowAllStories(groupPosition)) {
if (currentState == StateFilter.SAVED) { if (currentState == StateFilter.SAVED) {
// the existence of this row in saved mode is something of a framework artifact and may // the existence of this row in saved mode is something of a framework artifact and may
@ -456,17 +464,15 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
i = new Intent(getActivity(), ReadStoriesItemsList.class); i = new Intent(getActivity(), ReadStoriesItemsList.class);
} else if (adapter.isRowSavedStories(groupPosition)) { } else if (adapter.isRowSavedStories(groupPosition)) {
i = new Intent(getActivity(), SavedStoriesItemsList.class); i = new Intent(getActivity(), SavedStoriesItemsList.class);
} else if (adapter.isRowSavedSearches(groupPosition)) {
// group not clickable
return true;
} else { } else {
i = new Intent(getActivity(), FolderItemsList.class); i = new Intent(getActivity(), FolderItemsList.class);
String canonicalFolderName = adapter.getGroupFolderName(groupPosition); String canonicalFolderName = adapter.getGroupFolderName(groupPosition);
SessionDataSource sessionDataSource = getSessionData(fs, canonicalFolderName, null);
i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName); i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName);
i.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
adapter.lastFeedViewedId = null; adapter.lastFeedViewedId = null;
adapter.lastFolderViewed = canonicalFolderName; adapter.lastFolderViewed = canonicalFolderName;
} }
FeedSet fs = adapter.getGroup(groupPosition);
i.putExtra(ItemsList.EXTRA_FEED_SET, fs); i.putExtra(ItemsList.EXTRA_FEED_SET, fs);
startActivity(i); startActivity(i);
@ -541,7 +547,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
feedUtils.currentFolderName = folderName; feedUtils.currentFolderName = folderName;
} }
FeedItemsList.startActivity(getActivity(), fs, feed, folderName); SessionDataSource sessionDataSource = getSessionData(fs, folderName, feed);
FeedItemsList.startActivity(getActivity(), fs, feed, folderName, sessionDataSource);
adapter.lastFeedViewedId = feed.feedId; adapter.lastFeedViewedId = feed.feedId;
adapter.lastFolderViewed = null; adapter.lastFolderViewed = null;
} }
@ -619,4 +626,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
} }
@Nullable
private SessionDataSource getSessionData(FeedSet fs, String folderName, @Nullable Feed feed) {
if (PrefsUtils.loadNextOnMarkRead(requireContext())) {
Session activeSession = new Session(fs, folderName, feed);
return adapter.buildSessionDataSource(activeSession);
}
return null;
}
} }

View file

@ -1,15 +1,18 @@
package com.newsblur.fragment; package com.newsblur.fragment;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ReadingAction;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadingActionListener;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@ -24,29 +27,30 @@ public class ReadingActionConfirmationFragment extends DialogFragment {
private static final String DIALOG_TITLE = "dialog_title"; private static final String DIALOG_TITLE = "dialog_title";
private static final String DIALOG_MESSAGE = "dialog_message"; private static final String DIALOG_MESSAGE = "dialog_message";
private static final String DIALOG_CHOICES_RID = "dialog_choices_rid"; private static final String DIALOG_CHOICES_RID = "dialog_choices_rid";
private static final String FINISH_AFTER = "finish_after"; private static final String ACTION_CALLBACK = "action_callback";
public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, boolean finishAfter) { public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, @Nullable ReadingActionListener callback) {
ReadingActionConfirmationFragment fragment = new ReadingActionConfirmationFragment(); ReadingActionConfirmationFragment fragment = new ReadingActionConfirmationFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putSerializable(READING_ACTION, ra); args.putSerializable(READING_ACTION, ra);
args.putCharSequence(DIALOG_TITLE, title); args.putCharSequence(DIALOG_TITLE, title);
args.putCharSequence(DIALOG_MESSAGE, message); args.putCharSequence(DIALOG_MESSAGE, message);
args.putInt(DIALOG_CHOICES_RID, choicesId); args.putInt(DIALOG_CHOICES_RID, choicesId);
args.putBoolean(FINISH_AFTER, finishAfter); args.putSerializable(ACTION_CALLBACK, callback);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
final ReadingAction ra = (ReadingAction)getArguments().getSerializable(READING_ACTION); final ReadingAction ra = (ReadingAction) getArguments().getSerializable(READING_ACTION);
CharSequence title = getArguments().getCharSequence(DIALOG_TITLE); CharSequence title = getArguments().getCharSequence(DIALOG_TITLE);
CharSequence message = getArguments().getCharSequence(DIALOG_MESSAGE); CharSequence message = getArguments().getCharSequence(DIALOG_MESSAGE);
int choicesId = getArguments().getInt(DIALOG_CHOICES_RID); int choicesId = getArguments().getInt(DIALOG_CHOICES_RID);
final boolean finishAfter = getArguments().getBoolean(FINISH_AFTER); @Nullable ReadingActionListener callback = (ReadingActionListener) getArguments().getSerializable(ACTION_CALLBACK);
builder.setTitle(title); builder.setTitle(title);
// NB: setting a message will override the display of the buttons, making the dialogue a no-op // NB: setting a message will override the display of the buttons, making the dialogue a no-op
@ -54,9 +58,9 @@ public class ReadingActionConfirmationFragment extends DialogFragment {
builder.setItems(choicesId, new DialogInterface.OnClickListener() { builder.setItems(choicesId, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (which == 0) { if (which == 0) {
feedUtils.doAction(ra, getActivity()); feedUtils.doAction(ra, requireContext());
if (finishAfter) { if (callback != null) {
getActivity().finish(); callback.onReadingActionCompleted();
} }
} }
} }

View file

@ -419,7 +419,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
true true
} }
R.id.menu_go_to_feed -> { R.id.menu_go_to_feed -> {
FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null) FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null, null)
true true
} }
else -> { else -> {

View file

@ -95,4 +95,19 @@ public class AppConstants {
// Free standard account sites limit // Free standard account sites limit
public final static int FREE_ACCOUNT_SITE_LIMIT = 64; public final static int FREE_ACCOUNT_SITE_LIMIT = 64;
// The following keys are used to mark the position of the special meta-folders within
// the folders array. Since the ExpandableListView doesn't handle collapsing of views
// set to View.GONE, we have to totally remove any hidden groups from the group count
// and adjust all folder indicies accordingly. Fake folders are created with these
// very unlikely names and layout methods check against them before assuming a row is
// a normal folder. All the string comparison is a small price to pay to avoid the
// alternative of index-counting in a situation where some rows might be disabled.
public static final String GLOBAL_SHARED_STORIES_GROUP_KEY = "GLOBAL_SHARED_STORIES_GROUP_KEY";
public static final String ALL_SHARED_STORIES_GROUP_KEY = "ALL_SHARED_STORIES_GROUP_KEY";
public static final String ALL_STORIES_GROUP_KEY = "ALL_STORIES_GROUP_KEY";
public static final String INFREQUENT_SITE_STORIES_GROUP_KEY = "INFREQUENT_SITE_STORIES_GROUP_KEY";
public static final String READ_STORIES_GROUP_KEY = "READ_STORIES_GROUP_KEY";
public static final String SAVED_STORIES_GROUP_KEY = "SAVED_STORIES_GROUP_KEY";
public static final String SAVED_SEARCHES_GROUP_KEY = "SAVED_SEARCHES_GROUP_KEY";
} }

View file

@ -330,8 +330,8 @@ public class FeedSet implements Serializable {
if (!( o instanceof FeedSet)) return false; if (!( o instanceof FeedSet)) return false;
FeedSet s = (FeedSet) o; FeedSet s = (FeedSet) o;
if ( !TextUtils.equals(searchQuery, s.searchQuery)) return false; if ( !FeedUtils.textUtilsEquals(searchQuery, s.searchQuery)) return false;
if ( !TextUtils.equals(folderName, s.folderName)) return false; if ( !FeedUtils.textUtilsEquals(folderName, s.folderName)) return false;
if ( isFilterSaved != s.isFilterSaved ) return false; if ( isFilterSaved != s.isFilterSaved ) return false;
if ( (feeds != null) && (s.feeds != null) && s.feeds.equals(feeds) ) return true; if ( (feeds != null) && (s.feeds != null) && s.feeds.equals(feeds) ) return true;
if ( (socialFeeds != null) && (s.socialFeeds != null) && s.socialFeeds.equals(socialFeeds) ) return true; if ( (socialFeeds != null) && (s.socialFeeds != null) && s.socialFeeds.equals(socialFeeds) ) return true;

View file

@ -210,10 +210,13 @@ class FeedUtils(
triggerSync(context) triggerSync(context)
} }
fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int) =
markRead(activity, fs, olderThan, newerThan, choicesRid, null)
/** /**
* Marks some or all of the stories in a FeedSet as read for an activity, handling confirmation dialogues as necessary. * Marks some or all of the stories in a FeedSet as read for an activity, handling confirmation dialogues as necessary.
*/ */
fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int, finishAfter: Boolean) { fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int, callback: ReadingActionListener?) {
val ra: ReadingAction = if (fs.isAllNormal && (olderThan != null || newerThan != null)) { val ra: ReadingAction = if (fs.isAllNormal && (olderThan != null || newerThan != null)) {
// the mark-all-read API doesn't support range bounding, so we need to pass each and every // the mark-all-read API doesn't support range bounding, so we need to pass each and every
// feed ID to the API instead. // feed ID to the API instead.
@ -252,9 +255,7 @@ class FeedUtils(
} }
if (doImmediate) { if (doImmediate) {
doAction(ra, activity) doAction(ra, activity)
if (finishAfter) { callback?.onReadingActionCompleted()
activity.finish()
}
} else { } else {
val title: String? = when { val title: String? = when {
fs.isAllNormal -> { fs.isAllNormal -> {
@ -270,7 +271,7 @@ class FeedUtils(
dbHelper.getFeed(fs.singleFeed)?.title ?: "" dbHelper.getFeed(fs.singleFeed)?.title ?: ""
} }
} }
val dialog = ReadingActionConfirmationFragment.newInstance(ra, title, optionalOverrideMessage, choicesRid, finishAfter) val dialog = ReadingActionConfirmationFragment.newInstance(ra, title, optionalOverrideMessage, choicesRid, callback)
dialog.show(activity.supportFragmentManager, "dialog") dialog.show(activity.supportFragmentManager, "dialog")
} }
} }
@ -508,5 +509,23 @@ class FeedUtils(
val parts = TextUtils.split(storyHash, ":") val parts = TextUtils.split(storyHash, ":")
return if (parts.size != 2) null else parts[0] return if (parts.size != 2) null else parts[0]
} }
/**
* Copy of TextUtils.equals because of Java for unit tests
*/
@JvmStatic
fun textUtilsEquals(a: CharSequence?, b: CharSequence?): Boolean {
if (a === b) return true
return if (a != null && b != null && a.length == b.length) {
if (a is String && b is String) {
a == b
} else {
for (i in a.indices) {
if (a[i] != b[i]) return false
}
true
}
} else false
}
} }
} }

View file

@ -123,4 +123,5 @@ public class PrefConstants {
public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view"; public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view";
public static final String WIDGET_BACKGROUND = "widget_background"; public static final String WIDGET_BACKGROUND = "widget_background";
public static final String IN_APP_REVIEW = "in_app_review"; public static final String IN_APP_REVIEW = "in_app_review";
public static final String LOAD_NEXT_ON_MARK_READ = "load_next_on_mark_read";
} }

View file

@ -1064,4 +1064,9 @@ public class PrefsUtils {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0); SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return MarkStoryReadBehavior.valueOf(preferences.getString(PrefConstants.STORY_MARK_READ_BEHAVIOR, MarkStoryReadBehavior.IMMEDIATELY.name())); return MarkStoryReadBehavior.valueOf(preferences.getString(PrefConstants.STORY_MARK_READ_BEHAVIOR, MarkStoryReadBehavior.IMMEDIATELY.name()));
} }
public static boolean loadNextOnMarkRead(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return prefs.getBoolean(PrefConstants.LOAD_NEXT_ON_MARK_READ, false);
}
} }

View file

@ -0,0 +1,149 @@
package com.newsblur.util
import com.newsblur.domain.Feed
import java.io.Serializable
/**
* @return Set of folder keys that don't support
* mark all read action
*/
private val invalidMarkAllReadFolderKeys by lazy {
setOf(
AppConstants.GLOBAL_SHARED_STORIES_GROUP_KEY,
AppConstants.ALL_SHARED_STORIES_GROUP_KEY,
AppConstants.INFREQUENT_SITE_STORIES_GROUP_KEY,
AppConstants.READ_STORIES_GROUP_KEY,
AppConstants.SAVED_STORIES_GROUP_KEY,
AppConstants.SAVED_SEARCHES_GROUP_KEY,
)
}
/**
* As of writing this function, the zipping of the two sources
* is valid as the "activeFolderNames" and "activeFolderChildren"
* can be mapped by their folder name.
* @return Map of folder names to their feed list.
*/
private fun List<String>.zipFolderFeed(foldersChildren: List<List<Feed>>): Map<String, List<Feed>> {
val first = this.iterator()
val second = foldersChildren.iterator()
return buildMap {
while (first.hasNext() && second.hasNext()) {
this[first.next()] = second.next()
}
}
}
private fun Feed.toFeedSet() = FeedSet.singleFeed(this.feedId).apply {
isMuted = !this@toFeedSet.active
}
/**
* Represents the user's current reading session data source
* as constructed and filtered by the home list adapter
* based on settings and preferences.
*/
class SessionDataSource private constructor(
private val folders: List<String>,
private val foldersChildrenMap: Map<String, List<Feed>>
) : Serializable {
private lateinit var session: Session
constructor(
activeSession: Session,
folders: List<String>,
foldersChildren: List<List<Feed>>,
) : this(
folders = folders.filterNot { invalidMarkAllReadFolderKeys.contains(it) },
foldersChildrenMap = folders.zipFolderFeed(foldersChildren)
.filterNot { invalidMarkAllReadFolderKeys.contains(it.key) },
) {
this.session = activeSession
}
/**
* @return The next feed within a folder or null if the folder
* is showing the last feed.
*/
private fun getNextFolderFeed(feed: Feed, folderName: String): Feed? {
val cleanFolderName =
// ROOT FOLDER maps to ALL_STORIES_GROUP_KEY
if (folderName == AppConstants.ROOT_FOLDER)
AppConstants.ALL_STORIES_GROUP_KEY
else folderName
val folderFeeds = foldersChildrenMap[cleanFolderName]
return folderFeeds?.let { feeds ->
val feedIndex = feeds.indexOf(feed)
if (feedIndex == -1) return null // invalid feed
val nextFeedIndex = when (feedIndex) {
feeds.size - 1 -> null // null feed if EOL
in feeds.indices -> feedIndex + 1 // next feed
else -> null // no valid feed found
}
nextFeedIndex?.let { feeds[it] }
}
}
/**
* @return The next non empty folder and its feeds based on the given folder name.
* If the next folder doesn't have feeds, it will call itself with the new folder name
* until it finds a non empty folder or it will get to the end of the folder list.
*/
private fun getNextNonEmptyFolder(folderName: String): Pair<String, List<Feed>>? = with(folders.indexOf(folderName)) {
val nextIndex = if (this == folders.size - 1) {
0 // first folder if EOL
} else if (this in folders.indices) {
this + 1 // next folder
} else this // no folder found
val nextFolderName = if (nextIndex in folders.indices) {
folders[nextIndex]
} else null
if (nextFolderName == null || nextFolderName == folderName)
return null
val feeds = foldersChildrenMap[nextFolderName]
if (feeds == null || feeds.isEmpty())
// try and get the next non empty folder name
getNextNonEmptyFolder(nextFolderName)
else nextFolderName to feeds
}
fun getNextSession(): Session? = if (session.feedSet.isFolder) {
val folderName = session.feedSet.folderName
getNextNonEmptyFolder(folderName)?.let { (nextFolderName, nextFolderFeeds) ->
val nextFeedSet = FeedSet.folder(nextFolderName, nextFolderFeeds.map { it.feedId }.toSet())
Session(feedSet = nextFeedSet, folderName = nextFolderName).also { nextSession ->
session = nextSession
}
}
} else if (session.feed != null && session.folderName != null) {
val nextFeed = getNextFolderFeed(feed = session.feed!!, folderName = session.folderName!!)
nextFeed?.let {
Session(feedSet = it.toFeedSet(), session.folderName, it).also { nextSession ->
session = nextSession
}
}
} else null
}
/**
* Represents the user's current reading session.
*
* When reading a folder, [folderName] and [feed] will be null.
*
* When reading a feed, [folderName] and [feed] will be non null.
*/
data class Session(
val feedSet: FeedSet,
val folderName: String? = null,
val feed: Feed? = null,
) : Serializable
interface ReadingActionListener : Serializable {
fun onReadingActionCompleted()
}

View file

@ -0,0 +1,16 @@
package com.newsblur.viewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.newsblur.util.Session
class ItemListViewModel : ViewModel() {
private val _nextSession = MutableLiveData<Session>()
val nextSession: LiveData<Session> = _nextSession
fun updateSession(session: Session) {
_nextSession.value = session
}
}

View file

@ -0,0 +1,165 @@
package com.newsblur
import com.newsblur.domain.Feed
import com.newsblur.util.FeedSet
import com.newsblur.util.Session
import com.newsblur.util.SessionDataSource
import org.junit.Assert
import org.junit.Test
class SessionDataSourceTest {
private val folders = listOf(
"F1",
"F2",
"F3",
"F4",
"F5",
)
private val folderChildren = listOf(
emptyList(),
listOf(
createFeed("20"),
createFeed("21"),
createFeed("22"),
),
listOf(
createFeed("30"),
),
emptyList(),
listOf(
createFeed("50"),
createFeed("51"),
)
)
@Test
fun `session constructor test`() {
val feedSet = FeedSet.singleFeed("1")
val session = Session(feedSet)
Assert.assertEquals(feedSet, session.feedSet)
Assert.assertNull(session.feed)
Assert.assertNull(session.folderName)
}
@Test
fun `session full constructor test`() {
val feedSet = FeedSet.singleFeed("10")
val feed = createFeed("10")
val session = Session(feedSet, "folderName", feed)
Assert.assertEquals(feedSet, session.feedSet)
Assert.assertEquals("folderName", session.folderName)
Assert.assertEquals(feed, session.feed)
}
@Test
fun `next session for unknown feedId`() {
val session = Session(FeedSet.singleFeed("123"))
val sessionDs = SessionDataSource(session, folders, folderChildren)
Assert.assertNull(sessionDs.getNextSession())
}
@Test
fun `next session for empty folder`() {
val feedSet = FeedSet.singleFeed("123")
val feed = createFeed("123")
val session = Session(feedSet, "F1", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
Assert.assertNull(sessionDs.getNextSession())
}
/**
* Expected to return the next [Session] containing feed id 11
* as the second feed in folder F2 after feed id 10
*/
@Test
fun `next session for F2 feedSet`() {
val feedSet = FeedSet.singleFeed("20")
val feed = createFeed("20")
val session = Session(feedSet, "F2", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
sessionDs.getNextSession()?.let {
Assert.assertEquals("F2", it.folderName)
Assert.assertEquals("21", it.feed?.feedId)
with(it.feedSet) {
Assert.assertNotNull(this)
Assert.assertTrue(it.feedSet.flatFeedIds.size == 1)
Assert.assertEquals("21", it.feedSet.flatFeedIds.first())
}
} ?: Assert.fail("Next session was null")
}
/**
* Expected to return a null [Session] because feed id 12
* is the last feed id in folder F2
*/
@Test
fun `next session for end of F2 feedSet`() {
val feedSet = FeedSet.singleFeed("22")
val feed = createFeed("22")
val session = Session(feedSet, "F2", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
Assert.assertNull(sessionDs.getNextSession())
}
@Test
fun `next session for F2 feedSetFolder`() {
val feedSet = FeedSet.folder("F2", setOf("21"))
val feed = createFeed("21")
val session = Session(feedSet, "F2", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
sessionDs.getNextSession()?.let {
Assert.assertNull(it.feed)
Assert.assertEquals("F3", it.folderName)
Assert.assertEquals("F3", it.feedSet.folderName)
Assert.assertEquals("30", it.feedSet.flatFeedIds.firstOrNull())
} ?: Assert.fail("Next session is null for F2 feedSetFolder")
}
/**
* Expected to return folder "F5" because folder "F3"
* doesn't have any feeds
*/
@Test
fun `next session for F3 feedSetFolder`() {
val feedSet = FeedSet.folder("F3", setOf("30"))
val feed = createFeed("30")
val session = Session(feedSet, "F3", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
sessionDs.getNextSession()?.let {
Assert.assertNull(it.feed)
Assert.assertEquals("F5", it.folderName)
Assert.assertEquals("F5", it.feedSet.folderName)
Assert.assertEquals("50", it.feedSet.flatFeedIds.firstOrNull())
} ?: Assert.fail("Next session is null for F5 feedSetFolder")
}
/**
* Expected to return session for F1 feedSetFolder
*/
@Test
fun `next session for F5 feedSetFolder`() {
val feedSet = FeedSet.folder("F5", setOf("50"))
val feed = createFeed("50")
val session = Session(feedSet, "F5", feed)
val sessionDs = SessionDataSource(session, folders, folderChildren)
sessionDs.getNextSession()?.let {
Assert.assertNull(it.feed)
Assert.assertEquals("F2", it.folderName)
Assert.assertEquals("F2", it.feedSet.folderName)
Assert.assertEquals(setOf("20", "21", "22"), it.feedSet.flatFeedIds)
} ?: Assert.fail("Next session is null for F5 feedSetFolder")
}
private fun createFeed(id: String) = Feed().apply {
feedId = id
title = "Feed #$id"
}
}