summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/checks.yml14
-rw-r--r--.github/workflows/runEmulatorTests.sh3
-rw-r--r--app/src/main/AndroidManifest.xml2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java32
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java15
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java (renamed from app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java)27
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java138
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java17
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java178
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java86
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java42
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java125
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java155
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java122
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java150
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java89
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java24
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java105
-rw-r--r--app/src/main/res/layout/audioplayer_fragment.xml2
-rw-r--r--app/src/main/res/layout/episodes_list_fragment.xml4
-rw-r--r--app/src/main/res/layout/home_fragment.xml75
-rw-r--r--app/src/main/res/layout/home_section.xml91
-rw-r--r--app/src/main/res/layout/horizontal_feed_item.xml33
-rw-r--r--app/src/main/res/layout/horizontal_itemlist_item.xml123
-rw-r--r--app/src/main/res/layout/queue_fragment.xml3
-rw-r--r--app/src/main/res/layout/searchlist_item_feed.xml21
-rw-r--r--app/src/main/res/layout/simple_list_fragment.xml4
-rw-r--r--app/src/main/res/menu/home.xml25
-rw-r--r--app/src/main/res/xml/shortcuts.xml2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java86
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java102
-rw-r--r--core/src/main/res/drawable-anydpi-v26/ic_subscriptions_shortcut.xml (renamed from core/src/main/res/drawable-anydpi-v26/ic_folder_shortcut.xml)2
-rw-r--r--core/src/main/res/drawable/bg_circle.xml6
-rw-r--r--core/src/main/res/drawable/bg_pill.xml7
-rw-r--r--core/src/main/res/drawable/ic_arrow_down.xml7
-rw-r--r--core/src/main/res/drawable/ic_curved_arrow.xml9
-rw-r--r--core/src/main/res/drawable/ic_folder_black.xml5
-rw-r--r--core/src/main/res/drawable/ic_home.xml9
-rw-r--r--core/src/main/res/drawable/ic_shuffle.xml9
-rw-r--r--core/src/main/res/drawable/ic_subscriptions.xml10
-rw-r--r--core/src/main/res/drawable/ic_subscriptions_black.xml10
-rw-r--r--core/src/main/res/drawable/ic_subscriptions_shortcut.xml (renamed from core/src/main/res/drawable/ic_folder_shortcut.xml)2
-rw-r--r--core/src/main/res/layout/player_widget.xml15
-rw-r--r--core/src/main/res/values/arrays.xml19
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java2
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java159
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java10
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java16
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java40
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java30
-rw-r--r--ui/i18n/src/main/res/values/strings.xml12
-rw-r--r--ui/png-icons/src/main/res/drawable/ic_widget_skip.xml4
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java2
58 files changed, 2013 insertions, 286 deletions
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 749e2f812..96de69a1f 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -110,7 +110,7 @@ jobs:
runs-on: macOS-latest
timeout-minutes: 45
env:
- api-level: 27
+ api-level: 30
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
@@ -118,8 +118,6 @@ jobs:
with:
distribution: 'temurin'
java-version: '11'
- - name: Build with Gradle
- run: ./gradlew assemblePlayDebugAndroidTest
- name: Cache Gradle
uses: actions/cache@v3
with:
@@ -127,6 +125,8 @@ jobs:
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
+ - name: Build with Gradle
+ run: ./gradlew assemblePlayDebugAndroidTest
- name: Cache AVD
uses: actions/cache@v3
id: avd-cache
@@ -134,20 +134,24 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-${{ env.api-level }}
+ key: avd-${{ hashFiles('.github/workflows/*') }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.api-level }}
+ target: aosp_atd
+ channel: canary
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- disable-animations: false
+ disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Android Emulator test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.api-level }}
+ target: aosp_atd
+ channel: canary
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
diff --git a/.github/workflows/runEmulatorTests.sh b/.github/workflows/runEmulatorTests.sh
index 106b69444..c297932a7 100644
--- a/.github/workflows/runEmulatorTests.sh
+++ b/.github/workflows/runEmulatorTests.sh
@@ -4,8 +4,7 @@ set -o pipefail
runTests() {
./gradlew connectedPlayDebugAndroidTest connectedDebugAndroidTest \
- -Pandroid.testInstrumentationRunnerArguments.notAnnotation=de.test.antennapod.IgnoreOnCi \
- | grep -v "V/InstrumentationResultParser: INSTRUMENTATION_STATUS"
+ -Pandroid.testInstrumentationRunnerArguments.notAnnotation=de.test.antennapod.IgnoreOnCi
}
# Retry tests to make them less flaky
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 50ed82646..87375c9fc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -327,7 +327,7 @@
<activity android:name=".activity.SelectSubscriptionActivity"
android:label="@string/shortcut_subscription_label"
- android:icon="@drawable/ic_folder_shortcut"
+ android:icon="@drawable/ic_subscriptions_shortcut"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:exported="true">
<intent-filter>
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
index 837dcd731..5e570828c 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -9,8 +9,6 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
@@ -19,7 +17,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
@@ -32,14 +29,11 @@ import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.RecyclerView;
-
import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
-import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
-import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
@@ -47,14 +41,15 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RatingDialog;
+import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.AudioPlayerFragment;
+import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
import de.danoeh.antennapod.fragment.InboxFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
@@ -63,9 +58,11 @@ import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SearchFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.fragment.TransitionEffect;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.preferences.PreferenceUpgrader;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
+import de.danoeh.antennapod.ui.home.HomeFragment;
import de.danoeh.antennapod.view.LockableBottomSheetBehavior;
/**
@@ -222,13 +219,6 @@ public class MainActivity extends CastEnabledActivity {
private void checkFirstLaunch() {
SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) {
- loadFragment(AddFeedFragment.TAG, null);
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (drawerLayout != null) { // Tablet layout does not have a drawer
- drawerLayout.openDrawer(navDrawer);
- }
- }, 1500);
-
// for backward compatibility, we only change defaults for fresh installs
UserPreferences.setUpdateInterval(12);
AutoUpdateManager.restartUpdateAlarm(this);
@@ -249,6 +239,11 @@ public class MainActivity extends CastEnabledActivity {
public void setPlayerVisible(boolean visible) {
getBottomSheet().setLocked(!visible);
+ if (visible) {
+ bottomSheetCallback.onStateChanged(null, getBottomSheet().getState()); // Update toolbar visibility
+ } else {
+ getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
FragmentContainerView mainView = findViewById(R.id.main_view);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mainView.getLayoutParams();
params.setMargins(0, 0, 0, visible ? (int) getResources().getDimension(R.dimen.external_player_height) : 0);
@@ -264,6 +259,9 @@ public class MainActivity extends CastEnabledActivity {
Log.d(TAG, "loadFragment(tag: " + tag + ", args: " + args + ")");
Fragment fragment;
switch (tag) {
+ case HomeFragment.TAG:
+ fragment = new HomeFragment();
+ break;
case QueueFragment.TAG:
fragment = new QueueFragment();
break;
@@ -286,9 +284,9 @@ public class MainActivity extends CastEnabledActivity {
fragment = new SubscriptionFragment();
break;
default:
- // default to the queue
- fragment = new QueueFragment();
- tag = QueueFragment.TAG;
+ // default to home screen
+ fragment = new HomeFragment();
+ tag = HomeFragment.TAG;
args = null;
break;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java
index 52293091e..26fff9f01 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/SelectSubscriptionActivity.java
@@ -99,7 +99,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
if (bitmap != null) {
icon = IconCompat.createWithAdaptiveBitmap(bitmap);
} else {
- icon = IconCompat.createWithResource(this, R.drawable.ic_folder_shortcut);
+ icon = IconCompat.createWithResource(this, R.drawable.ic_subscriptions_shortcut);
}
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, id)
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
index 088caf70a..9c2ff2586 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
@@ -38,6 +38,7 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
private List<FeedItem> episodes = new ArrayList<>();
private FeedItem longPressedItem;
int longPressedPosition = 0; // used to init actionMode
+ private int dummyViews = 0;
public EpisodeItemListAdapter(MainActivity mainActivity) {
super(mainActivity);
@@ -45,6 +46,10 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
setHasStableIds(true);
}
+ public void setDummyViews(int dummyViews) {
+ this.dummyViews = dummyViews;
+ }
+
public void updateItems(List<FeedItem> items) {
episodes = items;
notifyDataSetChanged();
@@ -64,6 +69,11 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
@Override
public final void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
+ if (pos >= episodes.size()) {
+ holder.bindDummy();
+ return;
+ }
+
// Reset state of recycled views
holder.coverHolder.setVisibility(View.VISIBLE);
holder.dragHandle.setVisibility(View.GONE);
@@ -155,13 +165,16 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
@Override
public long getItemId(int position) {
+ if (position >= episodes.size()) {
+ return RecyclerView.NO_ID; // Dummy views
+ }
FeedItem item = episodes.get(position);
return item != null ? item.getId() : RecyclerView.NO_POSITION;
}
@Override
public int getItemCount() {
- return episodes.size();
+ return dummyViews + episodes.size();
}
protected FeedItem getItem(int index) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java
index 92865e211..3e0190ee5 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java
@@ -11,20 +11,25 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.ui.common.SquareImageView;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
-public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResultAdapter.Holder> {
-
+public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFeedListAdapter.Holder> {
private final WeakReference<MainActivity> mainActivityRef;
private final List<Feed> data = new ArrayList<>();
+ private int dummyViews = 0;
- public FeedSearchResultAdapter(MainActivity mainActivity) {
+ public HorizontalFeedListAdapter(MainActivity mainActivity) {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
+ public void setDummyViews(int dummyViews) {
+ this.dummyViews = dummyViews;
+ }
+
public void updateData(List<Feed> newData) {
data.clear();
data.addAll(newData);
@@ -34,12 +39,21 @@ public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResu
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View convertView = View.inflate(mainActivityRef.get(), R.layout.searchlist_item_feed, null);
+ View convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null);
return new Holder(convertView);
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
+ if (position >= data.size()) {
+ holder.itemView.setAlpha(0.1f);
+ Glide.with(mainActivityRef.get()).clear(holder.imageView);
+ holder.imageView.setImageResource(
+ ThemeUtils.getDrawableFromAttr(mainActivityRef.get(), android.R.attr.textColorSecondary));
+ return;
+ }
+
+ holder.itemView.setAlpha(1.0f);
final Feed podcast = data.get(position);
holder.imageView.setContentDescription(podcast.getTitle());
holder.imageView.setOnClickListener(v ->
@@ -56,12 +70,15 @@ public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResu
@Override
public long getItemId(int position) {
+ if (position >= data.size()) {
+ return RecyclerView.NO_ID; // Dummy views
+ }
return data.get(position).getId();
}
@Override
public int getItemCount() {
- return data.size();
+ return dummyViews + data.size();
}
static class Holder extends RecyclerView.ViewHolder {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java
new file mode 100644
index 000000000..4e8a2b05e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java
@@ -0,0 +1,138 @@
+package de.danoeh.antennapod.adapter;
+
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.fragment.ItemPagerFragment;
+import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+public class HorizontalItemListAdapter extends RecyclerView.Adapter<HorizontalItemViewHolder>
+ implements View.OnCreateContextMenuListener {
+ private final WeakReference<MainActivity> mainActivityRef;
+ private List<FeedItem> data = new ArrayList<>();
+ private FeedItem longPressedItem;
+ private int dummyViews = 0;
+
+ public HorizontalItemListAdapter(MainActivity mainActivity) {
+ this.mainActivityRef = new WeakReference<>(mainActivity);
+ setHasStableIds(true);
+ }
+
+ public void setDummyViews(int dummyViews) {
+ this.dummyViews = dummyViews;
+ }
+
+ public void updateData(List<FeedItem> newData) {
+ data = newData;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public HorizontalItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new HorizontalItemViewHolder(mainActivityRef.get(), parent);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull HorizontalItemViewHolder holder, int position) {
+ if (position >= data.size()) {
+ holder.bindDummy();
+ return;
+ }
+
+ final FeedItem item = data.get(position);
+ holder.bind(item);
+
+ holder.card.setOnCreateContextMenuListener(this);
+ holder.card.setOnLongClickListener(v -> {
+ longPressedItem = item;
+ return false;
+ });
+ holder.secondaryActionIcon.setOnCreateContextMenuListener(this);
+ holder.secondaryActionIcon.setOnLongClickListener(v -> {
+ longPressedItem = item;
+ return false;
+ });
+ holder.card.setOnClickListener(v -> {
+ MainActivity activity = mainActivityRef.get();
+ if (activity != null) {
+ long[] ids = FeedItemUtil.getIds(data);
+ int clickPosition = ArrayUtils.indexOf(ids, item.getId());
+ activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition));
+ }
+ });
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (position >= data.size()) {
+ return RecyclerView.NO_ID; // Dummy views
+ }
+ return data.get(position).getId();
+ }
+
+ @Override
+ public int getItemCount() {
+ return dummyViews + data.size();
+ }
+
+ @Override
+ public void onViewRecycled(@NonNull HorizontalItemViewHolder holder) {
+ super.onViewRecycled(holder);
+ // Set all listeners to null. This is required to prevent leaking fragments that have set a listener.
+ // Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected)
+ holder.card.setOnClickListener(null);
+ holder.card.setOnCreateContextMenuListener(null);
+ holder.card.setOnLongClickListener(null);
+ holder.secondaryActionIcon.setOnClickListener(null);
+ holder.secondaryActionIcon.setOnCreateContextMenuListener(null);
+ holder.secondaryActionIcon.setOnLongClickListener(null);
+ }
+
+ /**
+ * {@link #notifyItemChanged(int)} is final, so we can not override.
+ * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition.
+ * This causes flickering and breaks the download animation that stores the old progress in the View.
+ * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}.
+ * We actually ignore the payload and always do a full bind but calling the partial bind method ensures
+ * that ViewHolders are always re-used.
+ *
+ * @param position Position of the item that has changed
+ */
+ public void notifyItemChangedCompat(int position) {
+ notifyItemChanged(position, "foo");
+ }
+
+ @Nullable
+ public FeedItem getLongPressedItem() {
+ return longPressedItem;
+ }
+
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ MenuInflater inflater = mainActivityRef.get().getMenuInflater();
+ if (longPressedItem == null) {
+ return;
+ }
+ menu.clear();
+ inflater.inflate(R.menu.feeditemlist_context, menu);
+ menu.setHeaderTitle(longPressedItem.getTitle());
+ FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item);
+ }
+
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
index c0fc07ff6..c1df44da3 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java
@@ -36,6 +36,7 @@ import de.danoeh.antennapod.fragment.NavDrawerFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
+import de.danoeh.antennapod.ui.home.HomeFragment;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
@@ -112,6 +113,8 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
private @DrawableRes int getDrawable(String tag) {
switch (tag) {
+ case HomeFragment.TAG:
+ return R.drawable.ic_home;
case QueueFragment.TAG:
return R.drawable.ic_playlist_play;
case InboxFragment.TAG:
@@ -123,7 +126,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
case PlaybackHistoryFragment.TAG:
return R.drawable.ic_history;
case SubscriptionFragment.TAG:
- return R.drawable.ic_folder;
+ return R.drawable.ic_subscriptions;
case AddFeedFragment.TAG:
return R.drawable.ic_add;
default:
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
index fce5b0ddc..86ea98ff2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
@@ -163,8 +163,7 @@ public class AddFeedFragment extends Fragment {
private void performSearch() {
viewBinding.combinedFeedSearchEditText.clearFocus();
- InputMethodManager in = (InputMethodManager)
- getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ InputMethodManager in = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
in.hideSoftInputFromWindow(viewBinding.combinedFeedSearchEditText.getWindowToken(), 0);
String query = viewBinding.combinedFeedSearchEditText.getText().toString();
if (query.matches("http[s]?://.*")) {
@@ -172,6 +171,7 @@ public class AddFeedFragment extends Fragment {
return;
}
activity.loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher.class, query));
+ viewBinding.combinedFeedSearchEditText.post(() -> viewBinding.combinedFeedSearchEditText.setText(""));
}
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
index 95f08a838..7d8cadd02 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
@@ -13,7 +13,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -29,18 +28,19 @@ import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
-import de.danoeh.antennapod.event.FeedListUpdateEvent;
-import de.danoeh.antennapod.event.QueueEvent;
-import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
-import de.danoeh.antennapod.dialog.TagSettingsDialog;
-import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
-import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.dialog.RenameItemDialog;
+import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
+import de.danoeh.antennapod.dialog.TagSettingsDialog;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.event.QueueEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.ui.home.HomeFragment;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -65,6 +65,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
public static final String TAG = "NavDrawerFragment";
public static final String[] NAV_DRAWER_TAGS = {
+ HomeFragment.TAG,
QueueFragment.TAG,
InboxFragment.TAG,
AllEpisodesFragment.TAG,
@@ -430,7 +431,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
public static String getLastNavFragment(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
- String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, QueueFragment.TAG);
+ String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, HomeFragment.TAG);
Log.d(TAG, "getLastNavFragment() -> " + lastFragment);
return lastFragment;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
index 0142498a8..b40fa4281 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
@@ -27,7 +27,7 @@ import com.google.android.material.chip.Chip;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
-import de.danoeh.antennapod.adapter.FeedSearchResultAdapter;
+import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
@@ -65,7 +65,7 @@ public class SearchFragment extends Fragment {
private static final int SEARCH_DEBOUNCE_INTERVAL = 1500;
private EpisodeItemListAdapter adapter;
- private FeedSearchResultAdapter adapterFeeds;
+ private HorizontalFeedListAdapter adapterFeeds;
private Disposable disposable;
private ProgressBar progressBar;
private EmptyViewHandler emptyViewHandler;
@@ -144,7 +144,7 @@ public class SearchFragment extends Fragment {
LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity());
layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL);
recyclerViewFeeds.setLayoutManager(layoutManagerFeeds);
- adapterFeeds = new FeedSearchResultAdapter((MainActivity) getActivity());
+ adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity());
recyclerViewFeeds.setAdapter(adapterFeeds);
emptyViewHandler = new EmptyViewHandler(getContext());
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
index 6ee5d5c20..bb7d9ff30 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
@@ -247,7 +247,7 @@ public class SubscriptionFragment extends Fragment
private void setupEmptyView() {
emptyView = new EmptyViewHandler(getContext());
- emptyView.setIcon(R.drawable.ic_folder);
+ emptyView.setIcon(R.drawable.ic_subscriptions);
emptyView.setTitle(R.string.no_subscriptions_head_label);
emptyView.setMessage(R.string.no_subscriptions_label);
emptyView.attachToRecyclerView(subscriptionRecycler);
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java
new file mode 100644
index 000000000..a08907917
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java
@@ -0,0 +1,178 @@
+package de.danoeh.antennapod.ui.home;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentContainerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.event.DownloadEvent;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
+import de.danoeh.antennapod.databinding.HomeFragmentBinding;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.fragment.SearchFragment;
+import de.danoeh.antennapod.ui.home.sections.DownloadsSection;
+import de.danoeh.antennapod.ui.home.sections.EpisodesSurpriseSection;
+import de.danoeh.antennapod.ui.home.sections.InboxSection;
+import de.danoeh.antennapod.ui.home.sections.QueueSection;
+import de.danoeh.antennapod.ui.home.sections.SubscriptionsSection;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Shows unread or recently published episodes
+ */
+public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
+
+ public static final String TAG = "HomeFragment";
+ public static final String PREF_NAME = "PrefHomeFragment";
+ public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString";
+
+ private static final String KEY_UP_ARROW = "up_arrow";
+ private boolean displayUpArrow;
+ private HomeFragmentBinding viewBinding;
+ private Disposable disposable;
+
+ @NonNull
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ viewBinding = HomeFragmentBinding.inflate(inflater);
+ viewBinding.toolbar.inflateMenu(R.menu.home);
+ viewBinding.toolbar.setOnMenuItemClickListener(this);
+ if (savedInstanceState != null) {
+ displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
+ }
+ ((MainActivity) requireActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow);
+ refreshToolbarState();
+ populateSectionList();
+ updateWelcomeScreenVisibility();
+ return viewBinding.getRoot();
+ }
+
+ private void populateSectionList() {
+ viewBinding.homeContainer.removeAllViews();
+
+ List<String> hiddenSections = getHiddenSections(getContext());
+ String[] sectionTags = getResources().getStringArray(R.array.home_section_tags);
+ for (String sectionTag : sectionTags) {
+ if (hiddenSections.contains(sectionTag)) {
+ continue;
+ }
+ addSection(getSection(sectionTag));
+ }
+ }
+
+ private void addSection(Fragment section) {
+ FragmentContainerView containerView = new FragmentContainerView(getContext());
+ containerView.setId(View.generateViewId());
+ viewBinding.homeContainer.addView(containerView);
+ getChildFragmentManager().beginTransaction().add(containerView.getId(), section).commit();
+ }
+
+ private Fragment getSection(String tag) {
+ switch (tag) {
+ case QueueSection.TAG:
+ return new QueueSection();
+ case InboxSection.TAG:
+ return new InboxSection();
+ case EpisodesSurpriseSection.TAG:
+ return new EpisodesSurpriseSection();
+ case SubscriptionsSection.TAG:
+ return new SubscriptionsSection();
+ case DownloadsSection.TAG:
+ return new DownloadsSection();
+ default:
+ return null;
+ }
+ }
+
+ public static List<String> getHiddenSections(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
+ String hiddenSectionsString = prefs.getString(HomeFragment.PREF_HIDDEN_SECTIONS, "");
+ return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenSectionsString, ",")));
+ }
+
+ private void refreshToolbarState() {
+ MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(),
+ R.id.refresh_item, DownloadService.isRunning && DownloadService.isDownloadingFeeds());
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(DownloadEvent event) {
+ refreshToolbarState();
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() == R.id.homesettings_items) {
+ HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList());
+ return true;
+ } else if (item.getItemId() == R.id.refresh_item) {
+ AutoUpdateManager.runImmediate(requireContext());
+ return true;
+ } else if (item.getItemId() == R.id.action_search) {
+ ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onFeedListChanged(FeedListUpdateEvent event) {
+ updateWelcomeScreenVisibility();
+ }
+
+ private void updateWelcomeScreenVisibility() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(() -> DBReader.getNavDrawerData().items.size())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(numSubscriptions -> {
+ viewBinding.welcomeContainer.setVisibility(numSubscriptions == 0 ? View.VISIBLE : View.GONE);
+ viewBinding.homeContainer.setVisibility(numSubscriptions == 0 ? View.GONE : View.VISIBLE);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java
new file mode 100644
index 000000000..dd48f0ada
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java
@@ -0,0 +1,86 @@
+package de.danoeh.antennapod.ui.home;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
+import de.danoeh.antennapod.adapter.HorizontalItemListAdapter;
+import de.danoeh.antennapod.databinding.HomeSectionBinding;
+import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import org.greenrobot.eventbus.EventBus;
+
+/**
+ * Section on the HomeFragment
+ */
+public abstract class HomeSection extends Fragment implements View.OnCreateContextMenuListener {
+ public static final String TAG = "HomeSection";
+ protected HomeSectionBinding viewBinding;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ viewBinding = HomeSectionBinding.inflate(inflater);
+ viewBinding.titleLabel.setText(getSectionTitle());
+ viewBinding.moreButton.setText(getString(R.string.navigate_arrows, getMoreLinkTitle()));
+ viewBinding.moreButton.setOnClickListener((view) -> handleMoreClick());
+ if (TextUtils.isEmpty(getMoreLinkTitle())) {
+ viewBinding.moreButton.setVisibility(View.INVISIBLE);
+ }
+ return viewBinding.getRoot();
+ }
+
+ @Override
+ public boolean onContextItemSelected(@NonNull MenuItem item) {
+ if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) {
+ // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones.
+ // Apparently, none of the visibility check method works reliably on its own, so we just use all.
+ return false;
+ }
+ FeedItem longPressedItem;
+ if (viewBinding.recyclerView.getAdapter() instanceof EpisodeItemListAdapter) {
+ EpisodeItemListAdapter adapter = (EpisodeItemListAdapter) viewBinding.recyclerView.getAdapter();
+ longPressedItem = adapter.getLongPressedItem();
+ } else if (viewBinding.recyclerView.getAdapter() instanceof HorizontalItemListAdapter) {
+ HorizontalItemListAdapter adapter = (HorizontalItemListAdapter) viewBinding.recyclerView.getAdapter();
+ longPressedItem = adapter.getLongPressedItem();
+ } else {
+ return false;
+ }
+
+ if (longPressedItem == null) {
+ Log.i(TAG, "Selected item or listAdapter was null, ignoring selection");
+ return super.onContextItemSelected(item);
+ }
+ return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), longPressedItem);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ registerForContextMenu(viewBinding.recyclerView);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ unregisterForContextMenu(viewBinding.recyclerView);
+ }
+
+ protected abstract String getSectionTitle();
+
+ protected abstract String getMoreLinkTitle();
+
+ protected abstract void handleMoreClick();
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java
new file mode 100644
index 000000000..aa8ef8de3
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.ui.home;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+import androidx.appcompat.app.AlertDialog;
+import de.danoeh.antennapod.R;
+
+import java.util.List;
+
+public class HomeSectionsSettingsDialog {
+ public static void open(Context context, DialogInterface.OnClickListener onSettingsChanged) {
+ final List<String> hiddenSections = HomeFragment.getHiddenSections(context);
+ String[] sectionLabels = context.getResources().getStringArray(R.array.home_section_titles);
+ String[] sectionTags = context.getResources().getStringArray(R.array.home_section_tags);
+ final boolean[] checked = new boolean[sectionLabels.length];
+ for (int i = 0; i < sectionLabels.length; i++) {
+ String tag = sectionTags[i];
+ if (!hiddenSections.contains(tag)) {
+ checked[i] = true;
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.configure_home);
+ builder.setMultiChoiceItems(sectionLabels, checked, (dialog, which, isChecked) -> {
+ if (isChecked) {
+ hiddenSections.remove(sectionTags[which]);
+ } else {
+ hiddenSections.add(sectionTags[which]);
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(HomeFragment.PREF_HIDDEN_SECTIONS, TextUtils.join(",", hiddenSections)).apply();
+ onSettingsChanged.onClick(dialog, which);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.create().show();
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java
new file mode 100644
index 000000000..42b97294f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java
@@ -0,0 +1,125 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
+import de.danoeh.antennapod.core.event.DownloadLogEvent;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.ui.home.HomeSection;
+import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+
+public class DownloadsSection extends HomeSection {
+ public static final String TAG = "DownloadsSection";
+ private static final int NUM_EPISODES = 2;
+ private EpisodeItemListAdapter adapter;
+ private List<FeedItem> items;
+ private Disposable disposable;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ viewBinding.recyclerView.setPadding(0, 0, 0, 0);
+ viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
+ viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false));
+ viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool());
+ adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuItemUtils.setOnClickListeners(menu, DownloadsSection.this::onContextItemSelected);
+ }
+ };
+ adapter.setDummyViews(NUM_EPISODES);
+ viewBinding.recyclerView.setAdapter(adapter);
+ loadItems();
+ return view;
+ }
+
+ @Override
+ protected void handleMoreClick() {
+ ((MainActivity) requireActivity()).loadChildFragment(new CompletedDownloadsFragment());
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(FeedItemEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(PlaybackPositionEvent event) {
+ if (adapter == null) {
+ return;
+ }
+ for (int i = 0; i < adapter.getItemCount(); i++) {
+ EpisodeItemViewHolder holder = (EpisodeItemViewHolder)
+ viewBinding.recyclerView.findViewHolderForAdapterPosition(i);
+ if (holder != null && holder.isCurrentlyPlayingItem()) {
+ holder.notifyPlaybackPositionUpdated(event);
+ break;
+ }
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onDownloadLogChanged(DownloadLogEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onPlayerStatusChanged(PlayerStatusEvent event) {
+ loadItems();
+ }
+
+ @Override
+ protected String getSectionTitle() {
+ return getString(R.string.home_downloads_title);
+ }
+
+ @Override
+ protected String getMoreLinkTitle() {
+ return getString(R.string.downloads_label);
+ }
+
+ private void loadItems() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(DBReader::getDownloadedItems)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(downloads -> {
+ if (downloads.size() > NUM_EPISODES) {
+ downloads = downloads.subList(0, NUM_EPISODES);
+ }
+ items = downloads;
+ adapter.setDummyViews(0);
+ adapter.updateItems(items);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java
new file mode 100644
index 000000000..680bb5ef4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java
@@ -0,0 +1,155 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.HorizontalItemListAdapter;
+import de.danoeh.antennapod.core.event.DownloadEvent;
+import de.danoeh.antennapod.core.event.DownloaderUpdate;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.fragment.AllEpisodesFragment;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.ui.home.HomeSection;
+import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+import java.util.Random;
+
+public class EpisodesSurpriseSection extends HomeSection {
+ public static final String TAG = "EpisodesSurpriseSection";
+ private static final int NUM_EPISODES = 8;
+ private static int seed = 0;
+ private HorizontalItemListAdapter listAdapter;
+ private Disposable disposable;
+ private List<FeedItem> episodes;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ viewBinding.shuffleButton.setVisibility(View.VISIBLE);
+ viewBinding.shuffleButton.setOnClickListener(v -> {
+ seed = new Random().nextInt();
+ viewBinding.recyclerView.scrollToPosition(0);
+ loadItems();
+ });
+ listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuItemUtils.setOnClickListeners(menu, EpisodesSurpriseSection.this::onContextItemSelected);
+ }
+ };
+ listAdapter.setDummyViews(NUM_EPISODES);
+ viewBinding.recyclerView.setLayoutManager(
+ new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false));
+ viewBinding.recyclerView.setAdapter(listAdapter);
+ if (seed == 0) {
+ seed = new Random().nextInt();
+ }
+ loadItems();
+ return view;
+ }
+
+ @Override
+ protected void handleMoreClick() {
+ ((MainActivity) requireActivity()).loadChildFragment(new AllEpisodesFragment());
+ }
+
+ @Override
+ protected String getSectionTitle() {
+ return getString(R.string.home_surprise_title);
+ }
+
+ @Override
+ protected String getMoreLinkTitle() {
+ return getString(R.string.episodes_label);
+ }
+
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onPlayerStatusChanged(PlayerStatusEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(FeedItemEvent event) {
+ Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
+ if (episodes == null) {
+ return;
+ }
+ for (int i = 0, size = event.items.size(); i < size; i++) {
+ FeedItem item = event.items.get(i);
+ int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId());
+ if (pos >= 0) {
+ episodes.remove(pos);
+ episodes.add(pos, item);
+ listAdapter.notifyItemChangedCompat(pos);
+ }
+ }
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(DownloadEvent event) {
+ Log.d(TAG, "onEventMainThread() called with DownloadEvent");
+ DownloaderUpdate update = event.update;
+ if (listAdapter != null && update.mediaIds.length > 0) {
+ for (long mediaId : update.mediaIds) {
+ int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId);
+ if (pos >= 0) {
+ listAdapter.notifyItemChangedCompat(pos);
+ }
+ }
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(PlaybackPositionEvent event) {
+ if (listAdapter == null) {
+ return;
+ }
+ for (int i = 0; i < listAdapter.getItemCount(); i++) {
+ HorizontalItemViewHolder holder = (HorizontalItemViewHolder)
+ viewBinding.recyclerView.findViewHolderForAdapterPosition(i);
+ if (holder != null && holder.isCurrentlyPlayingItem()) {
+ holder.notifyPlaybackPositionUpdated(event);
+ break;
+ }
+ }
+ }
+
+ private void loadItems() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(() -> DBReader.getRandomEpisodes(NUM_EPISODES, seed))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(episodes -> {
+ this.episodes = episodes;
+ listAdapter.setDummyViews(0);
+ listAdapter.updateData(episodes);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java
new file mode 100644
index 000000000..d05735acb
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java
@@ -0,0 +1,122 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
+import de.danoeh.antennapod.core.event.DownloadEvent;
+import de.danoeh.antennapod.core.event.DownloaderUpdate;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.fragment.InboxFragment;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import de.danoeh.antennapod.ui.home.HomeSection;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+
+public class InboxSection extends HomeSection {
+ public static final String TAG = "InboxSection";
+ private static final int NUM_EPISODES = 2;
+ private EpisodeItemListAdapter adapter;
+ private List<FeedItem> items;
+ private Disposable disposable;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ viewBinding.recyclerView.setPadding(0, 0, 0, 0);
+ viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
+ viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false));
+ viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool());
+ adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuItemUtils.setOnClickListeners(menu, InboxSection.this::onContextItemSelected);
+ }
+ };
+ adapter.setDummyViews(NUM_EPISODES);
+ viewBinding.recyclerView.setAdapter(adapter);
+ loadItems();
+ return view;
+ }
+
+ @Override
+ protected void handleMoreClick() {
+ ((MainActivity) requireActivity()).loadChildFragment(new InboxFragment());
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(FeedItemEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(DownloadEvent event) {
+ Log.d(TAG, "onEventMainThread() called with DownloadEvent");
+ DownloaderUpdate update = event.update;
+ if (adapter != null && update.mediaIds.length > 0) {
+ for (long mediaId : update.mediaIds) {
+ int pos = FeedItemUtil.indexOfItemWithMediaId(items, mediaId);
+ if (pos >= 0) {
+ adapter.notifyItemChangedCompat(pos);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected String getSectionTitle() {
+ return getString(R.string.home_new_title);
+ }
+
+ @Override
+ protected String getMoreLinkTitle() {
+ return getString(R.string.inbox_label);
+ }
+
+ private void loadItems() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(() ->
+ new Pair<>(DBReader.getNewItemsList(0, NUM_EPISODES),
+ PodDBAdapter.getInstance().getNumberOfNewItems()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(data -> {
+ items = data.first;
+ adapter.setDummyViews(0);
+ adapter.updateItems(items);
+ viewBinding.numNewItemsLabel.setVisibility(View.VISIBLE);
+ viewBinding.numNewItemsLabel.setText(String.valueOf(data.second));
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java
new file mode 100644
index 000000000..efff7927e
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java
@@ -0,0 +1,150 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.HorizontalItemListAdapter;
+import de.danoeh.antennapod.core.event.DownloadEvent;
+import de.danoeh.antennapod.core.event.DownloaderUpdate;
+import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.event.FeedItemEvent;
+import de.danoeh.antennapod.event.PlayerStatusEvent;
+import de.danoeh.antennapod.event.QueueEvent;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.fragment.QueueFragment;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.ui.home.HomeSection;
+import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+
+public class QueueSection extends HomeSection {
+ public static final String TAG = "QueueSection";
+ private static final int NUM_EPISODES = 8;
+ private HorizontalItemListAdapter listAdapter;
+ private Disposable disposable;
+ private List<FeedItem> queue;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) {
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ MenuItemUtils.setOnClickListeners(menu, QueueSection.this::onContextItemSelected);
+ }
+ };
+ listAdapter.setDummyViews(NUM_EPISODES);
+ viewBinding.recyclerView.setLayoutManager(
+ new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false));
+ viewBinding.recyclerView.setAdapter(listAdapter);
+ loadItems();
+ return view;
+ }
+
+ @Override
+ protected void handleMoreClick() {
+ ((MainActivity) requireActivity()).loadChildFragment(new QueueFragment());
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onQueueChanged(QueueEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onPlayerStatusChanged(PlayerStatusEvent event) {
+ loadItems();
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(FeedItemEvent event) {
+ Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
+ if (queue == null) {
+ return;
+ }
+ for (int i = 0, size = event.items.size(); i < size; i++) {
+ FeedItem item = event.items.get(i);
+ int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId());
+ if (pos >= 0) {
+ queue.remove(pos);
+ queue.add(pos, item);
+ listAdapter.notifyItemChangedCompat(pos);
+ }
+ }
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(DownloadEvent event) {
+ Log.d(TAG, "onEventMainThread() called with DownloadEvent");
+ DownloaderUpdate update = event.update;
+ if (listAdapter != null && update.mediaIds.length > 0) {
+ for (long mediaId : update.mediaIds) {
+ int pos = FeedItemUtil.indexOfItemWithMediaId(queue, mediaId);
+ if (pos >= 0) {
+ listAdapter.notifyItemChangedCompat(pos);
+ }
+ }
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEventMainThread(PlaybackPositionEvent event) {
+ if (listAdapter == null) {
+ return;
+ }
+ for (int i = 0; i < listAdapter.getItemCount(); i++) {
+ HorizontalItemViewHolder holder = (HorizontalItemViewHolder)
+ viewBinding.recyclerView.findViewHolderForAdapterPosition(i);
+ if (holder != null && holder.isCurrentlyPlayingItem()) {
+ holder.notifyPlaybackPositionUpdated(event);
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected String getSectionTitle() {
+ return getString(R.string.home_continue_title);
+ }
+
+ @Override
+ protected String getMoreLinkTitle() {
+ return getString(R.string.queue_label);
+ }
+
+ private void loadItems() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(() -> DBReader.getPausedQueue(NUM_EPISODES))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(queue -> {
+ this.queue = queue;
+ listAdapter.setDummyViews(0);
+ listAdapter.updateData(queue);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java
new file mode 100644
index 000000000..81dddbff3
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.event.FeedListUpdateEvent;
+import de.danoeh.antennapod.fragment.SubscriptionFragment;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.ui.home.HomeSection;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SubscriptionsSection extends HomeSection {
+ public static final String TAG = "SubscriptionsSection";
+ private static final int NUM_FEEDS = 8;
+ private HorizontalFeedListAdapter listAdapter;
+ private Disposable disposable;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ viewBinding.recyclerView.setLayoutManager(
+ new LinearLayoutManager(getActivity(), RecyclerView.HORIZONTAL, false));
+ listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity());
+ listAdapter.setDummyViews(NUM_FEEDS);
+ viewBinding.recyclerView.setAdapter(listAdapter);
+ loadItems();
+ return view;
+ }
+
+ @Override
+ protected void handleMoreClick() {
+ ((MainActivity) requireActivity()).loadChildFragment(new SubscriptionFragment());
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onFeedListChanged(FeedListUpdateEvent event) {
+ loadItems();
+ }
+
+ @Override
+ protected String getSectionTitle() {
+ return getString(R.string.home_classics_title);
+ }
+
+ @Override
+ protected String getMoreLinkTitle() {
+ return getString(R.string.subscriptions_label);
+ }
+
+ private void loadItems() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(() -> DBReader.getStatistics(true, 0, Long.MAX_VALUE).feedTime)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(statisticsData -> {
+ Collections.sort(statisticsData, (item1, item2) ->
+ Long.compare(item2.timePlayed, item1.timePlayed));
+ List<Feed> feeds = new ArrayList<>();
+ for (int i = 0; i < statisticsData.size() && i < NUM_FEEDS; i++) {
+ feeds.add(statisticsData.get(i).feed);
+ }
+ listAdapter.setDummyViews(0);
+ listAdapter.updateData(feeds);
+ }, error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
index 3839241d7..bc29740b0 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
@@ -198,6 +198,30 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
}
}
+ public void bindDummy() {
+ container.setAlpha(0.1f);
+ secondaryActionIcon.setImageDrawable(null);
+ isInbox.setVisibility(View.VISIBLE);
+ isVideo.setVisibility(View.GONE);
+ isFavorite.setVisibility(View.GONE);
+ isInQueue.setVisibility(View.GONE);
+ title.setText("โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ");
+ pubDate.setText("โ–ˆโ–ˆโ–ˆโ–ˆ");
+ duration.setText("โ–ˆโ–ˆโ–ˆโ–ˆ");
+ secondaryActionProgress.setPercentage(0, null);
+ progressBar.setVisibility(View.GONE);
+ position.setVisibility(View.GONE);
+ dragHandle.setVisibility(View.GONE);
+ size.setText("");
+ itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground));
+ placeholder.setText("");
+ new CoverLoader(activity)
+ .withResource(ThemeUtils.getDrawableFromAttr(activity, android.R.attr.textColorSecondary))
+ .withPlaceholderView(placeholder)
+ .withCoverView(cover)
+ .load();
+ }
+
private void updateDuration(PlaybackPositionEvent event) {
int currentPosition = event.getPosition();
int timeDuration = event.getDuration();
diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java
new file mode 100644
index 000000000..9723417ce
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java
@@ -0,0 +1,105 @@
+package de.danoeh.antennapod.view.viewholder;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.CoverLoader;
+import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton;
+import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
+import de.danoeh.antennapod.core.service.download.DownloadRequest;
+import de.danoeh.antennapod.core.service.download.DownloadService;
+import de.danoeh.antennapod.core.util.DateFormatter;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.ui.common.CircularProgressBar;
+import de.danoeh.antennapod.ui.common.SquareImageView;
+
+public class HorizontalItemViewHolder extends RecyclerView.ViewHolder {
+ public final View card;
+ public final ImageView secondaryActionIcon;
+ private final SquareImageView cover;
+ private final TextView title;
+ private final TextView date;
+ private final ProgressBar progressBar;
+ private final CircularProgressBar circularProgressBar;
+
+ private final MainActivity activity;
+ private FeedItem item;
+
+ public HorizontalItemViewHolder(MainActivity activity, ViewGroup parent) {
+ super(LayoutInflater.from(activity).inflate(R.layout.horizontal_itemlist_item, parent, false));
+ this.activity = activity;
+
+ card = itemView.findViewById(R.id.card);
+ cover = itemView.findViewById(R.id.cover);
+ title = itemView.findViewById(R.id.titleLabel);
+ date = itemView.findViewById(R.id.dateLabel);
+ secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon);
+ circularProgressBar = itemView.findViewById(R.id.circularProgressBar);
+ progressBar = itemView.findViewById(R.id.progressBar);
+ itemView.setTag(this);
+ }
+
+ public void bind(FeedItem item) {
+ this.item = item;
+
+ card.setAlpha(1.0f);
+ new CoverLoader(activity)
+ .withUri(ImageResourceUtils.getEpisodeListImageLocation(item))
+ .withFallbackUri(item.getFeed().getImageUrl())
+ .withCoverView(cover)
+ .load();
+ title.setText(item.getTitle());
+ date.setText(DateFormatter.formatAbbrev(activity, item.getPubDate()));
+ ItemActionButton actionButton = ItemActionButton.forItem(item);
+ actionButton.configure(secondaryActionIcon, secondaryActionIcon, activity);
+ secondaryActionIcon.setFocusable(false);
+
+ FeedMedia media = item.getMedia();
+ if (media == null) {
+ circularProgressBar.setPercentage(0, item);
+ } else {
+ if (item.getMedia().getDuration() > 0) {
+ progressBar.setProgress(100 * item.getMedia().getPosition() / item.getMedia().getDuration());
+ }
+ if (DownloadService.isDownloadingFile(media.getDownload_url())) {
+ final DownloadRequest downloadRequest = DownloadService.findRequest(media.getDownload_url());
+ float percent = 0.01f * downloadRequest.getProgressPercent();
+ circularProgressBar.setPercentage(Math.max(percent, 0.01f), item);
+ } else if (media.isDownloaded()) {
+ circularProgressBar.setPercentage(1, item); // Do not animate 100% -> 0%
+ } else {
+ circularProgressBar.setPercentage(0, item); // Animate X% -> 0%
+ }
+ }
+ }
+
+ public void bindDummy() {
+ card.setAlpha(0.1f);
+ new CoverLoader(activity)
+ .withResource(android.R.color.transparent)
+ .withCoverView(cover)
+ .load();
+ title.setText("โ–ˆโ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ");
+ date.setText("โ–ˆโ–ˆโ–ˆ");
+ secondaryActionIcon.setImageDrawable(null);
+ circularProgressBar.setPercentage(0, null);
+ progressBar.setProgress(50);
+ }
+
+ public boolean isCurrentlyPlayingItem() {
+ return item.getMedia() != null && FeedItemUtil.isCurrentlyPlaying(item.getMedia());
+ }
+
+ public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) {
+ progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration()));
+ }
+}
diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml
index 4e4ab389c..2008a8f33 100644
--- a/app/src/main/res/layout/audioplayer_fragment.xml
+++ b/app/src/main/res/layout/audioplayer_fragment.xml
@@ -14,7 +14,7 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationContentDescription="@string/toolbar_back_button_content_description"
- app:navigationIcon="?homeAsUpIndicator" />
+ app:navigationIcon="@drawable/ic_arrow_down" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/playerFragment"
diff --git a/app/src/main/res/layout/episodes_list_fragment.xml b/app/src/main/res/layout/episodes_list_fragment.xml
index 0a6c1da5f..629b7ab0e 100644
--- a/app/src/main/res/layout/episodes_list_fragment.xml
+++ b/app/src/main/res/layout/episodes_list_fragment.xml
@@ -2,6 +2,7 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -12,7 +13,8 @@
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:minHeight="?attr/actionBarSize"
- android:theme="?attr/actionBarTheme" />
+ android:theme="?attr/actionBarTheme"
+ app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/txtvInformation"
diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml
new file mode 100644
index 000000000..040e3df34
--- /dev/null
+++ b/app/src/main/res/layout/home_fragment.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ app:title="@string/home_label"
+ app:navigationIcon="?homeAsUpIndicator" />
+
+ <LinearLayout
+ android:id="@+id/welcomeContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone"
+ android:paddingHorizontal="32dp">
+
+ <ImageView
+ android:layout_width="80dp"
+ android:layout_height="80dp"
+ android:layout_marginBottom="8dp"
+ android:layout_gravity="start"
+ android:src="@drawable/ic_curved_arrow" />
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="16dp"
+ android:src="@mipmap/ic_launcher" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/home_welcome_title"
+ android:layout_marginBottom="8dp"
+ android:layout_gravity="center_horizontal"
+ android:textAlignment="center"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/home_welcome_text"
+ android:layout_gravity="center_horizontal"
+ android:textAlignment="center"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp" />
+
+ </LinearLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/homeContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="12dp" />
+
+ </androidx.core.widget.NestedScrollView>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/home_section.xml b/app/src/main/res/layout/home_section.xml
new file mode 100644
index 000000000..783688f92
--- /dev/null
+++ b/app/src/main/res/layout/home_section.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:paddingBottom="4dp">
+
+ <TextView
+ android:id="@+id/titleLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@id/moreButton"
+ android:layout_alignBottom="@id/moreButton"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginStart="16dp"
+ android:gravity="center"
+ android:textAlignment="center"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="18sp"
+ tools:text="Title" />
+
+ <ImageButton
+ android:id="@+id/shuffleButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/titleLabel"
+ android:layout_alignParentTop="true"
+ android:layout_marginVertical="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_toEndOf="@+id/titleLabel"
+ android:layout_toRightOf="@+id/titleLabel"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:visibility="gone"
+ tools:visibility="visible"
+ app:srcCompat="@drawable/ic_shuffle" />
+
+ <TextView
+ android:id="@+id/numNewItemsLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="20dp"
+ android:layout_alignBottom="@+id/titleLabel"
+ android:layout_alignParentTop="true"
+ android:layout_marginVertical="12dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginLeft="8dp"
+ android:layout_toEndOf="@+id/titleLabel"
+ android:layout_toRightOf="@+id/titleLabel"
+ android:background="@drawable/bg_pill"
+ android:gravity="center"
+ android:paddingHorizontal="8dp"
+ android:textAlignment="center"
+ android:textColor="?attr/colorPrimary"
+ android:textSize="16sp"
+ android:visibility="gone"
+ tools:visibility="visible"
+ tools:text="6" />
+
+ <Button
+ android:id="@+id/moreButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="0dp"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:paddingVertical="0dp"
+ android:layout_marginEnd="16dp"
+ tools:text="@string/discover_more"
+ style="@style/Widget.MaterialComponents.Button.TextButton" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/moreButton"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentBottom="true"
+ android:clipToPadding="false"
+ android:clipToOutline="false"
+ android:clipChildren="false"
+ android:paddingHorizontal="16dp" />
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/horizontal_feed_item.xml b/app/src/main/res/layout/horizontal_feed_item.xml
new file mode 100644
index 000000000..56a3b317d
--- /dev/null
+++ b/app/src/main/res/layout/horizontal_feed_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:padding="4dp"
+ android:clipToPadding="false"
+ android:clipToOutline="false"
+ android:clipChildren="false">
+
+ <androidx.cardview.widget.CardView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardBackgroundColor="@color/non_square_icon_background"
+ app:cardCornerRadius="16dp"
+ app:cardPreventCornerOverlap="false"
+ app:cardElevation="2dp">
+
+ <de.danoeh.antennapod.ui.common.SquareImageView
+ android:id="@+id/discovery_cover"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:elevation="4dp"
+ android:outlineProvider="bounds"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:background="?android:attr/windowBackground"
+ squareImageView:direction="height" />
+
+ </androidx.cardview.widget.CardView>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/horizontal_itemlist_item.xml b/app/src/main/res/layout/horizontal_itemlist_item.xml
new file mode 100644
index 000000000..a0e4e3d72
--- /dev/null
+++ b/app/src/main/res/layout/horizontal_itemlist_item.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:padding="4dp">
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/card"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ app:cardCornerRadius="12dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/background_elevated"
+ android:orientation="vertical">
+
+ <androidx.cardview.widget.CardView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardCornerRadius="12dp"
+ app:cardElevation="0dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#dddddd"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="128dp"
+ android:layout_height="128dp"
+ android:background="@color/image_readability_tint">
+
+ <de.danoeh.antennapod.ui.common.SquareImageView
+ android:id="@+id/cover"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:outlineProvider="bounds"
+ tools:src="@tools:sample/avatars"
+ squareImageView:direction="width" />
+
+ <ImageView
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="8dp"
+ android:padding="3dp"
+ app:srcCompat="@drawable/bg_circle" />
+
+ <de.danoeh.antennapod.ui.common.CircularProgressBar
+ android:id="@+id/circularProgressBar"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_margin="8dp"
+ android:layout_gravity="bottom|end"
+ app:foregroundColor="?attr/colorOnPrimary" />
+
+ <ImageView
+ android:id="@+id/secondaryActionIcon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="8dp"
+ android:padding="12dp"
+ android:clickable="true"
+ android:foreground="?attr/selectableItemBackgroundBorderless"
+ app:tintMode="src_atop"
+ app:tint="?attr/colorOnPrimary"
+ app:srcCompat="@drawable/ic_play_24dp" />
+
+ </FrameLayout>
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:max="100"
+ android:layout_gravity="bottom"
+ style="?attr/progressBarTheme"
+ tools:background="@android:color/holo_blue_light" />
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+ <TextView
+ android:id="@+id/titleLabel"
+ android:layout_width="128dp"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:paddingHorizontal="4dp"
+ android:layout_marginTop="4dp"
+ android:lines="2"
+ android:singleLine="false"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp"
+ tools:text="@sample/episodes.json/data/title" />
+
+ <TextView
+ android:id="@+id/dateLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:paddingHorizontal="4dp"
+ android:singleLine="true"
+ android:textAlignment="textStart"
+ android:textSize="14sp"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle" />
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml
index 292b1bb45..7c8bdefbf 100644
--- a/app/src/main/res/layout/queue_fragment.xml
+++ b/app/src/main/res/layout/queue_fragment.xml
@@ -13,7 +13,8 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true"
- app:title="@string/queue_label" />
+ app:title="@string/queue_label"
+ app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/info_bar"
diff --git a/app/src/main/res/layout/searchlist_item_feed.xml b/app/src/main/res/layout/searchlist_item_feed.xml
deleted file mode 100644
index c16911f99..000000000
--- a/app/src/main/res/layout/searchlist_item_feed.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
- android:layout_width="match_parent"
- android:layout_height="96dp"
- android:padding="4dp"
- android:clipToPadding="false">
-
- <de.danoeh.antennapod.ui.common.SquareImageView
- android:id="@+id/discovery_cover"
- android:layout_width="match_parent"
- android:layout_height="96dp"
- android:elevation="4dp"
- android:outlineProvider="bounds"
- android:foreground="?android:attr/selectableItemBackground"
- android:background="?android:attr/windowBackground"
- squareImageView:direction="height" />
-
-</LinearLayout>
-
diff --git a/app/src/main/res/layout/simple_list_fragment.xml b/app/src/main/res/layout/simple_list_fragment.xml
index 6ea3ab54b..5019edcfd 100644
--- a/app/src/main/res/layout/simple_list_fragment.xml
+++ b/app/src/main/res/layout/simple_list_fragment.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -10,7 +11,8 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
- android:layout_alignParentTop="true" />
+ android:layout_alignParentTop="true"
+ app:navigationIcon="?homeAsUpIndicator" />
<de.danoeh.antennapod.view.EpisodeItemListRecyclerView
android:id="@+id/recyclerView"
diff --git a/app/src/main/res/menu/home.xml b/app/src/main/res/menu/home.xml
new file mode 100644
index 000000000..f80218c0c
--- /dev/null
+++ b/app/src/main/res/menu/home.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/action_search"
+ android:icon="@drawable/ic_search"
+ custom:showAsAction="always"
+ android:title="@string/search_label"/>
+
+ <item
+ android:id="@+id/refresh_item"
+ android:title="@string/refresh_label"
+ android:menuCategory="container"
+ custom:showAsAction="always"
+ android:icon="@drawable/ic_refresh"/>
+
+ <item
+ android:id="@+id/homesettings_items"
+ android:icon="@drawable/ic_settings"
+ android:menuCategory="container"
+ android:title="@string/configure_home"
+ custom:showAsAction="never"/>
+
+</menu>
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index c802e2bba..7ca3b3787 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -32,7 +32,7 @@
<shortcut
android:enabled="true"
- android:icon="@drawable/ic_folder_shortcut"
+ android:icon="@drawable/ic_subscriptions_shortcut"
android:shortcutId="subscriptions"
android:shortcutShortLabel="@string/subscriptions_label">
<intent
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
index e7e8e9587..8241a2ca5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -7,7 +7,6 @@ import androidx.collection.ArrayMap;
import android.text.TextUtils;
import android.util.Log;
-import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -397,6 +396,18 @@ public final class DBReader {
}
}
+ public static List<FeedItem> getRandomEpisodes(int limit, int seed) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getRandomEpisodesCursor(limit, seed)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
public static int getTotalEpisodeCount(FeedItemFilter filter) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
@@ -616,6 +627,19 @@ public final class DBReader {
}
}
+ @NonNull
+ public static List<FeedItem> getPausedQueue(int limit) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getPausedQueueCursor(limit)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
/**
* Loads a specific FeedItem from the database.
*
@@ -852,52 +876,34 @@ public final class DBReader {
adapter.open();
StatisticsResult result = new StatisticsResult();
- List<Feed> feeds = getFeedList();
- for (Feed feed : feeds) {
- long feedPlayedTime = 0;
- long feedTotalTime = 0;
- long episodes = 0;
- long episodesStarted = 0;
- long totalDownloadSize = 0;
- long episodesDownloadCount = 0;
- List<FeedItem> items = getFeed(feed.getId()).getItems();
- for (FeedItem item : items) {
- FeedMedia media = item.getMedia();
- if (media == null) {
- continue;
- }
-
- if (media.getLastPlayedTime() > 0 && media.getPlayedDuration() != 0) {
- result.oldestDate = Math.min(result.oldestDate, media.getLastPlayedTime());
- }
- if (media.getLastPlayedTime() >= timeFilterFrom
- && media.getLastPlayedTime() <= timeFilterTo) {
- if (media.getPlayedDuration() != 0) {
- feedPlayedTime += media.getPlayedDuration() / 1000;
- } else if (includeMarkedAsPlayed && item.isPlayed()) {
- feedPlayedTime += media.getDuration() / 1000;
- }
- }
+ try (Cursor cursor = adapter.getFeedStatisticsCursor(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo)) {
+ int indexOldestDate = cursor.getColumnIndexOrThrow("oldest_date");
+ int indexNumEpisodes = cursor.getColumnIndexOrThrow("num_episodes");
+ int indexEpisodesStarted = cursor.getColumnIndexOrThrow("episodes_started");
+ int indexTotalTime = cursor.getColumnIndexOrThrow("total_time");
+ int indexPlayedTime = cursor.getColumnIndexOrThrow("played_time");
+ int indexNumDownloaded = cursor.getColumnIndexOrThrow("num_downloaded");
+ int indexDownloadSize = cursor.getColumnIndexOrThrow("download_size");
- boolean markedAsStarted = item.isPlayed() || media.getPosition() != 0;
- boolean hasStatistics = media.getPlaybackCompletionDate() != null || media.getPlayedDuration() > 0;
- if (hasStatistics || (includeMarkedAsPlayed && markedAsStarted)) {
- episodesStarted++;
- }
+ while (cursor.moveToNext()) {
+ Feed feed = extractFeedFromCursorRow(cursor);
- feedTotalTime += media.getDuration() / 1000;
+ long feedPlayedTime = Long.parseLong(cursor.getString(indexPlayedTime)) / 1000;
+ long feedTotalTime = Long.parseLong(cursor.getString(indexTotalTime)) / 1000;
+ long episodes = Long.parseLong(cursor.getString(indexNumEpisodes));
+ long episodesStarted = Long.parseLong(cursor.getString(indexEpisodesStarted));
+ long totalDownloadSize = Long.parseLong(cursor.getString(indexDownloadSize));
+ long episodesDownloadCount = Long.parseLong(cursor.getString(indexNumDownloaded));
+ long oldestDate = Long.parseLong(cursor.getString(indexOldestDate));
- if (media.isDownloaded()) {
- totalDownloadSize += new File(media.getFile_url()).length();
- episodesDownloadCount++;
+ if (episodes > 0 && oldestDate < Long.MAX_VALUE) {
+ result.oldestDate = Math.min(result.oldestDate, oldestDate);
}
- episodes++;
+ result.feedTime.add(new StatisticsItem(feed, feedTotalTime, feedPlayedTime, episodes,
+ episodesStarted, totalDownloadSize, episodesDownloadCount));
}
- result.feedTime.add(new StatisticsItem(feed, feedTotalTime, feedPlayedTime, episodes,
- episodesStarted, totalDownloadSize, episodesDownloadCount));
}
-
adapter.close();
return result;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
index 7f2742ab0..12377791e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java
@@ -8,6 +8,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -116,38 +117,23 @@ public class FeedItemPermutors {
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
*
- * The Smart Shuffle algorithm involves spreading episodes from each feed out over the whole
- * queue. To do this, we calculate the number of episodes in each feed, then a common multiple
- * (not the smallest); each episode is then spread out, and we sort the resulting list of
- * episodes by "spread out factor" and feed name.
+ * Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
+ * This method first starts with a queue of the final size, where each slot is empty (null).
+ * It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
+ * The podcast with the second-most number of episodes (`D`) is then
+ * placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
+ * This continues, until we end up with: `EEBEDEECEDEEAEE`.
*
- * For example, given a queue containing three episodes each from three different feeds
- * (A, B, and C), a simple pubdate sort might result in a queue that looks like the following:
- *
- * B1, B2, B3, A1, A2, C1, C2, C3, A3
- *
- * (note that feed B episodes were all published before the first feed A episode, so a simple
- * pubdate sort will often result in significant clustering of episodes from a single feed)
- *
- * Using Smart Shuffle, the resulting queue would look like the following:
- *
- * A1, B1, C1, A2, B2, C2, A3, B3, C3
- *
- * (note that episodes above <i>aren't strictly ordered in terms of pubdate</i>, but episodes
- * of each feed <b>do</b> appear in pubdate order)
+ * Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
*
* @param queue A (modifiable) list of FeedItem elements to be reordered.
* @param ascending {@code true} to use ascending pubdate in the reordering;
* {@code false} for descending.
*/
private static void smartShuffle(List<FeedItem> queue, boolean ascending) {
-
// Divide FeedItems into lists by feed
-
Map<Long, List<FeedItem>> map = new HashMap<>();
-
- while (!queue.isEmpty()) {
- FeedItem item = queue.remove(0);
+ for (FeedItem item : queue) {
Long id = item.getFeedId();
if (!map.containsKey(id)) {
map.put(id, new ArrayList<>());
@@ -156,55 +142,43 @@ public class FeedItemPermutors {
}
// Sort each individual list by PubDate (ascending/descending)
-
Comparator<FeedItem> itemComparator = ascending
- ? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
- : (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
-
- // Calculate the spread
-
- long spread = 0;
+ ? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
+ : (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
+ List<List<FeedItem>> feeds = new ArrayList<>();
for (Map.Entry<Long, List<FeedItem>> mapEntry : map.entrySet()) {
- List<FeedItem> feedItems = mapEntry.getValue();
- Collections.sort(feedItems, itemComparator);
- if (spread == 0) {
- spread = feedItems.size();
- } else if (spread % feedItems.size() != 0){
- spread *= feedItems.size();
- }
+ Collections.sort(mapEntry.getValue(), itemComparator);
+ feeds.add(mapEntry.getValue());
}
- // Create a list of the individual FeedItems lists, and sort it by feed title (ascending).
- // Doing this ensures that the feed order we use is predictable/deterministic.
-
- List<List<FeedItem>> feeds = new ArrayList<>(map.values());
- Collections.sort(feeds,
- (f1, f2) -> f1.get(0).getFeed().getTitle().compareTo(f2.get(0).getFeed().getTitle()));
+ ArrayList<Integer> emptySlots = new ArrayList<>();
+ for (int i = 0; i < queue.size(); i++) {
+ queue.set(i, null);
+ emptySlots.add(i);
+ }
- // Spread each episode out
- Map<Long, List<FeedItem>> spreadItems = new HashMap<>();
+ // Starting with the largest feed, place items spread out through the empty slots in the queue
+ Collections.sort(feeds, (f1, f2) -> Integer.compare(f2.size(), f1.size()));
for (List<FeedItem> feedItems : feeds) {
- long thisSpread = spread / feedItems.size();
- if (thisSpread == 0) {
- thisSpread = 1;
- }
- // Starting from 0 ensures we front-load, so the queue starts with one episode from
- // each feed in the queue
- long itemSpread = 0;
- for (FeedItem feedItem : feedItems) {
- if (!spreadItems.containsKey(itemSpread)) {
- spreadItems.put(itemSpread, new ArrayList<>());
+ double spread = (double) emptySlots.size() / (feedItems.size() + 1);
+ Iterator<Integer> emptySlotIterator = emptySlots.iterator();
+ int skipped = 0;
+ int placed = 0;
+ while (emptySlotIterator.hasNext()) {
+ int nextEmptySlot = emptySlotIterator.next();
+ skipped++;
+ if (skipped >= spread * (placed + 1)) {
+ if (queue.get(nextEmptySlot) != null) {
+ throw new RuntimeException("Slot to be placed in not empty");
+ }
+ queue.set(nextEmptySlot, feedItems.get(placed));
+ emptySlotIterator.remove();
+ placed++;
+ if (placed == feedItems.size()) {
+ break;
+ }
}
- spreadItems.get(itemSpread).add(feedItem);
- itemSpread += thisSpread;
}
}
-
- // Go through the spread items and add them to the queue
- List<Long> spreads = new ArrayList<>(spreadItems.keySet());
- Collections.sort(spreads);
- for (long itemSpread : spreads) {
- queue.addAll(spreadItems.get(itemSpread));
- }
}
}
diff --git a/core/src/main/res/drawable-anydpi-v26/ic_folder_shortcut.xml b/core/src/main/res/drawable-anydpi-v26/ic_subscriptions_shortcut.xml
index 0ee30ab73..10f437917 100644
--- a/core/src/main/res/drawable-anydpi-v26/ic_folder_shortcut.xml
+++ b/core/src/main/res/drawable-anydpi-v26/ic_subscriptions_shortcut.xml
@@ -3,7 +3,7 @@
<background android:drawable="@color/grey100" />
<foreground>
<inset
- android:drawable="@drawable/ic_folder_black"
+ android:drawable="@drawable/ic_subscriptions_black"
android:inset="33.3%" />
</foreground>
</adaptive-icon> \ No newline at end of file
diff --git a/core/src/main/res/drawable/bg_circle.xml b/core/src/main/res/drawable/bg_circle.xml
new file mode 100644
index 000000000..0957db5e4
--- /dev/null
+++ b/core/src/main/res/drawable/bg_circle.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?attr/colorPrimary" />
+ <corners android:radius="30dp" />
+ <size android:width="60dp" android:height="60dp"/>
+</shape>
diff --git a/core/src/main/res/drawable/bg_pill.xml b/core/src/main/res/drawable/bg_pill.xml
new file mode 100644
index 000000000..f5865ccff
--- /dev/null
+++ b/core/src/main/res/drawable/bg_pill.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="1dp"
+ android:color="?attr/colorPrimary" />
+ <corners android:radius="20dp" />
+</shape> \ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_arrow_down.xml b/core/src/main/res/drawable/ic_arrow_down.xml
new file mode 100644
index 000000000..187aa79c7
--- /dev/null
+++ b/core/src/main/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path android:fillColor="?attr/action_icon_color" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
+</vector>
diff --git a/core/src/main/res/drawable/ic_curved_arrow.xml b/core/src/main/res/drawable/ic_curved_arrow.xml
new file mode 100644
index 000000000..e0baab50a
--- /dev/null
+++ b/core/src/main/res/drawable/ic_curved_arrow.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:autoMirrored="true">
+ <path android:strokeColor="?attr/action_icon_color" android:strokeAlpha="0.4" android:strokeWidth="0.4" android:pathData="M 24.563 19.667 C 20.794 22.382 13.26 21.82 11.04 17.74 C 8.82 13.66 16.36 4.77 20.17 8.59 C 23.98 12.4 16.78 16.34 11.93 15.72 C 7.08 15.1 4.792 10.756 2.54 4.87"/>
+ <path android:fillColor="?attr/action_icon_color" android:fillAlpha="0.4" android:pathData="M 0.608 5.581 L 4.568 4.368 L 1.183 0.599" />
+</vector>
diff --git a/core/src/main/res/drawable/ic_folder_black.xml b/core/src/main/res/drawable/ic_folder_black.xml
deleted file mode 100644
index 8096fa582..000000000
--- a/core/src/main/res/drawable/ic_folder_black.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector android:height="24dp"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#000000" android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
-</vector>
diff --git a/core/src/main/res/drawable/ic_home.xml b/core/src/main/res/drawable/ic_home.xml
new file mode 100644
index 000000000..dc5a8dd52
--- /dev/null
+++ b/core/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/action_icon_color"
+ android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+</vector>
diff --git a/core/src/main/res/drawable/ic_shuffle.xml b/core/src/main/res/drawable/ic_shuffle.xml
new file mode 100644
index 000000000..085397444
--- /dev/null
+++ b/core/src/main/res/drawable/ic_shuffle.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/action_icon_color"
+ android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z"/>
+</vector>
diff --git a/core/src/main/res/drawable/ic_subscriptions.xml b/core/src/main/res/drawable/ic_subscriptions.xml
new file mode 100644
index 000000000..325f38450
--- /dev/null
+++ b/core/src/main/res/drawable/ic_subscriptions.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/action_icon_color"
+ android:pathData="M3,3v8h8V3H3zM9,9H5V5h4V9zM3,13v8h8v-8H3zM9,19H5v-4h4V19zM13,3v8h8V3H13zM19,9h-4V5h4V9zM13,13v8h8v-8H13zM19,19h-4v-4h4V19z"/>
+</vector> \ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_subscriptions_black.xml b/core/src/main/res/drawable/ic_subscriptions_black.xml
new file mode 100644
index 000000000..b8200c5ba
--- /dev/null
+++ b/core/src/main/res/drawable/ic_subscriptions_black.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M3,3v8h8V3H3zM9,9H5V5h4V9zM3,13v8h8v-8H3zM9,19H5v-4h4V19zM13,3v8h8V3H13zM19,9h-4V5h4V9zM13,13v8h8v-8H13zM19,19h-4v-4h4V19z"/>
+</vector> \ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_folder_shortcut.xml b/core/src/main/res/drawable/ic_subscriptions_shortcut.xml
index 2906c2795..b932aebaf 100644
--- a/core/src/main/res/drawable/ic_folder_shortcut.xml
+++ b/core/src/main/res/drawable/ic_subscriptions_shortcut.xml
@@ -2,6 +2,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_shortcut_background" />
<item
- android:drawable="@drawable/ic_folder_black"
+ android:drawable="@drawable/ic_subscriptions_black"
android:gravity="center" />
</layer-list> \ No newline at end of file
diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml
index 6f3842e8b..164ca80f8 100644
--- a/core/src/main/res/layout/player_widget.xml
+++ b/core/src/main/res/layout/player_widget.xml
@@ -92,50 +92,55 @@
android:id="@+id/butPlaybackSpeed"
android:layout_width="36dp"
android:layout_height="36dp"
+ android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/playback_speed"
android:layout_marginEnd="2dp"
- android:scaleType="fitXY"
+ android:scaleType="centerInside"
android:src="@drawable/ic_widget_playback_speed" />
<ImageButton
android:id="@+id/butRew"
android:layout_width="36dp"
android:layout_height="36dp"
+ android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/rewind_label"
android:layout_marginEnd="2dp"
- android:scaleType="fitXY"
+ android:scaleType="centerInside"
android:src="@drawable/ic_widget_fast_rewind" />
<ImageButton
android:id="@+id/butPlayExtended"
android:layout_width="36dp"
android:layout_height="36dp"
+ android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/play_label"
android:layout_marginEnd="2dp"
- android:scaleType="fitXY"
+ android:scaleType="centerInside"
android:src="@drawable/ic_widget_play" />
<ImageButton
android:id="@+id/butFastForward"
android:layout_width="36dp"
android:layout_height="36dp"
+ android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/fast_forward_label"
android:layout_marginEnd="2dp"
- android:scaleType="fitXY"
+ android:scaleType="centerInside"
android:src="@drawable/ic_widget_fast_forward" />
<ImageButton
android:id="@+id/butSkip"
android:layout_width="36dp"
android:layout_height="36dp"
+ android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/skip_episode_label"
android:layout_marginEnd="2dp"
- android:scaleType="fitXY"
+ android:scaleType="centerInside"
android:src="@drawable/ic_widget_skip" />
</LinearLayout>
diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index e5b4a5e3b..4a32eb760 100644
--- a/core/src/main/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
@@ -150,6 +150,7 @@
</string-array>
<string-array name="nav_drawer_titles">
+ <item>@string/home_label</item>
<item>@string/queue_label</item>
<item>@string/inbox_label</item>
<item>@string/episodes_label</item>
@@ -188,6 +189,22 @@
<item>3</item>
</string-array>
+ <string-array name="home_section_titles">
+ <item>@string/home_continue_title</item>
+ <item>@string/home_new_title</item>
+ <item>@string/home_surprise_title</item>
+ <item>@string/home_classics_title</item>
+ <item>@string/home_downloads_title</item>
+ </string-array>
+
+ <string-array name="home_section_tags">
+ <item>QueueSection</item>
+ <item>InboxSection</item>
+ <item>EpisodesSurpriseSection</item>
+ <item>SubscriptionsSection</item>
+ <item>DownloadsSection</item>
+ </string-array>
+
<string-array name="media_player_options">
<item>@string/media_player_exoplayer_recommended</item>
<item>@string/media_player_builtin</item>
@@ -265,6 +282,7 @@
</string-array>
<string-array name="back_button_go_to_pages">
+ <item>@string/home_label</item>
<item>@string/queue_label</item>
<item>@string/inbox_label</item>
<item>@string/episodes_label</item>
@@ -272,6 +290,7 @@
</string-array>
<string-array name="back_button_go_to_pages_tags">
+ <item>HomeFragment</item>
<item>QueueFragment</item>
<item>InboxFragment</item>
<item>EpisodesFragment</item>
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
index e0c940c56..d46cf6081 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
@@ -46,7 +46,7 @@ public class FeedItemFilter implements Serializable {
this(TextUtils.split(properties, ","));
}
- public FeedItemFilter(String[] properties) {
+ public FeedItemFilter(String... properties) {
this.properties = properties;
// see R.arrays.feed_filter_values
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
index 453d1c184..42517e972 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
@@ -227,46 +227,6 @@ public class PodDBAdapter {
+ KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
/**
- * Select all columns from the feed-table
- */
- private static final String[] FEED_SEL_STD = {
- TABLE_NAME_FEEDS + "." + KEY_ID,
- TABLE_NAME_FEEDS + "." + KEY_TITLE,
- TABLE_NAME_FEEDS + "." + KEY_CUSTOM_TITLE,
- TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
- TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
- TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
- TABLE_NAME_FEEDS + "." + KEY_LINK,
- TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION,
- TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK,
- TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE,
- TABLE_NAME_FEEDS + "." + KEY_LANGUAGE,
- TABLE_NAME_FEEDS + "." + KEY_AUTHOR,
- TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL,
- TABLE_NAME_FEEDS + "." + KEY_TYPE,
- TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER,
- TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED,
- TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED,
- TABLE_NAME_FEEDS + "." + KEY_IS_PAGED,
- TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK,
- TABLE_NAME_FEEDS + "." + KEY_USERNAME,
- TABLE_NAME_FEEDS + "." + KEY_PASSWORD,
- TABLE_NAME_FEEDS + "." + KEY_HIDE,
- TABLE_NAME_FEEDS + "." + KEY_SORT_ORDER,
- TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED,
- TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION,
- TABLE_NAME_FEEDS + "." + KEY_FEED_VOLUME_ADAPTION,
- TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER,
- TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER,
- TABLE_NAME_FEEDS + "." + KEY_MINIMAL_DURATION_FILTER,
- TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED,
- TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS,
- TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO,
- TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING,
- TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION
- };
-
- /**
* All the tables in the database
*/
private static final String[] ALL_TABLES = {
@@ -281,6 +241,7 @@ public class PodDBAdapter {
public static final String SELECT_KEY_ITEM_ID = "item_id";
public static final String SELECT_KEY_MEDIA_ID = "media_id";
+ public static final String SELECT_KEY_FEED_ID = "feed_id";
private static final String KEYS_FEED_ITEM_WITHOUT_DESCRIPTION =
TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " AS " + SELECT_KEY_ITEM_ID + ", "
@@ -312,6 +273,42 @@ public class PodDBAdapter {
+ TABLE_NAME_FEED_MEDIA + "." + KEY_HAS_EMBEDDED_PICTURE + ", "
+ TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME;
+ private static final String KEYS_FEED =
+ TABLE_NAME_FEEDS + "." + KEY_ID + " AS " + SELECT_KEY_FEED_ID + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_TITLE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_CUSTOM_TITLE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FILE_URL + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_LINK + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_LANGUAGE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_AUTHOR + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_TYPE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_IS_PAGED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_USERNAME + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_PASSWORD + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_HIDE + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_SORT_ORDER + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_VOLUME_ADAPTION + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_MINIMAL_DURATION_FILTER + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING + ", "
+ + TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION;
+
private static final String JOIN_FEED_ITEM_AND_MEDIA = " LEFT JOIN " + TABLE_NAME_FEED_MEDIA
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " ";
@@ -914,8 +911,10 @@ public class PodDBAdapter {
* @return The cursor of the query
*/
public final Cursor getAllFeedsCursor() {
- return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null,
- KEY_TITLE + " COLLATE NOCASE ASC");
+ final String query = "SELECT " + KEYS_FEED
+ + " FROM " + TABLE_NAME_FEEDS
+ + " ORDER BY " + TABLE_NAME_FEEDS + "." + KEY_TITLE + " COLLATE NOCASE ASC";
+ return db.rawQuery(query, null);
}
public final Cursor getFeedCursorDownloadUrls() {
@@ -995,6 +994,17 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
+ public final Cursor getPausedQueueCursor(int limit) {
+ //playback position > 0 (paused), rank by last played, then rest of queue
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_QUEUE
+ + " ON " + SELECT_KEY_ITEM_ID + " = " + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
+ + " ORDER BY " + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + ">0 DESC , "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + " DESC , " + TABLE_NAME_QUEUE + "." + KEY_ID
+ + " LIMIT " + limit;
+ return db.rawQuery(query, null);
+ }
+
public final Cursor getFavoritesCursor(int offset, int limit) {
final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ " INNER JOIN " + TABLE_NAME_FAVORITES
@@ -1052,6 +1062,25 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
+ public Cursor getRandomEpisodesCursor(int limit, int seed) {
+ final String allItemsRandomOrder = SELECT_FEED_ITEMS_AND_MEDIA
+ + " WHERE (" + KEY_READ + " = " + FeedItem.NEW + " OR " + KEY_READ + " = " + FeedItem.UNPLAYED + ") "
+ // Only from the last two years. Older episodes frequently contain broken covers and stuff like that
+ + " AND " + KEY_PUBDATE + " > " + (System.currentTimeMillis() - 1000L * 3600L * 24L * 356L * 2)
+ + " ORDER BY " + randomEpisodeNumber(seed);
+ final String query = "SELECT * FROM (" + allItemsRandomOrder + ")"
+ + " GROUP BY " + KEY_FEED
+ + " ORDER BY " + randomEpisodeNumber(seed * 3) + " DESC LIMIT " + limit;
+ return db.rawQuery(query, null);
+ }
+
+ /**
+ * SQLite does not support random seeds. Create our own "random" number based on that seed and the item ID
+ */
+ private String randomEpisodeNumber(int seed) {
+ return "((" + SELECT_KEY_ITEM_ID + " * " + seed + ") % 46471)";
+ }
+
public final Cursor getTotalEpisodeCountCursor(FeedItemFilter filter) {
String filterQuery = FeedItemFilterQuery.generateFrom(filter);
String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery;
@@ -1102,8 +1131,10 @@ public class PodDBAdapter {
}
public final Cursor getFeedCursor(final long id) {
- return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null,
- null, null, null);
+ final String query = "SELECT " + KEYS_FEED
+ + " FROM " + TABLE_NAME_FEEDS
+ + " WHERE " + SELECT_KEY_FEED_ID + " = " + id;
+ return db.rawQuery(query, null);
}
public final Cursor getFeedItemCursor(final String id) {
@@ -1167,6 +1198,46 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
+ public final Cursor getFeedStatisticsCursor(boolean includeMarkedAsPlayed, long timeFilterFrom, long timeFilterTo) {
+ final String lastPlayedTime = TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME;
+ String wasStarted = TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYBACK_COMPLETION_DATE + " > 0"
+ + " AND " + TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYED_DURATION + " > 0";
+ if (includeMarkedAsPlayed) {
+ wasStarted = "(" + wasStarted + ") OR "
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.PLAYED + " OR "
+ + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + "> 0";
+ }
+ final String timeFilter = lastPlayedTime + ">=" + timeFilterFrom
+ + " AND " + lastPlayedTime + "<=" + timeFilterTo;
+ String playedTime = TABLE_NAME_FEED_MEDIA + "." + KEY_PLAYED_DURATION;
+ if (includeMarkedAsPlayed) {
+ playedTime = "(CASE WHEN " + playedTime + " != 0"
+ + " THEN " + playedTime + " ELSE ("
+ + "CASE WHEN " + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.PLAYED
+ + " THEN " + TABLE_NAME_FEED_MEDIA + "." + KEY_DURATION + " ELSE 0 END"
+ + ") END)";
+ }
+
+ final String query = "SELECT " + KEYS_FEED + ", "
+ + "COUNT(*) AS num_episodes, "
+ + "MIN(CASE WHEN " + lastPlayedTime + " > 0"
+ + " THEN " + lastPlayedTime + " ELSE " + Long.MAX_VALUE + " END) AS oldest_date, "
+ + "SUM(CASE WHEN (" + wasStarted + ") THEN 1 ELSE 0 END) AS episodes_started, "
+ + "IFNULL(SUM(CASE WHEN (" + timeFilter + ")"
+ + " THEN (" + playedTime + ") ELSE 0 END), 0) AS played_time, "
+ + "IFNULL(SUM(" + TABLE_NAME_FEED_MEDIA + "." + KEY_DURATION + "), 0) AS total_time, "
+ + "SUM(CASE WHEN " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " > 0"
+ + " THEN 1 ELSE 0 END) AS num_downloaded, "
+ + "SUM(CASE WHEN " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " > 0"
+ + " THEN " + TABLE_NAME_FEED_MEDIA + "." + KEY_SIZE + " ELSE 0 END) AS download_size"
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + JOIN_FEED_ITEM_AND_MEDIA
+ + " INNER JOIN " + TABLE_NAME_FEEDS
+ + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ + " GROUP BY " + TABLE_NAME_FEEDS + "." + KEY_ID;
+ return db.rawQuery(query, null);
+ }
+
public int getQueueSize() {
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
Cursor c = db.rawQuery(query, null);
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
index 71e67812d..b48a7f9d1 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/ChapterCursorMapper.java
@@ -14,11 +14,11 @@ public abstract class ChapterCursorMapper {
*/
@NonNull
public static Chapter convert(@NonNull Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
- int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
- int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
- int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_TITLE);
+ int indexStart = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_START);
+ int indexLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LINK);
+ int indexImage = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
long id = cursor.getLong(indexId);
String title = cursor.getString(indexTitle);
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
index 4a5a792af..1b8f3c726 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java
@@ -17,14 +17,14 @@ public abstract class DownloadStatusCursorMapper {
*/
@NonNull
public static DownloadStatus convert(@NonNull Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
- int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE);
- int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE);
- int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL);
- int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON);
- int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE);
- int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED);
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ID);
+ int indexTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE);
+ int indexFeedFile = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEEDFILE);
+ int indexFileFileType = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEEDFILETYPE);
+ int indexSuccessful = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_SUCCESSFUL);
+ int indexReason = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_REASON);
+ int indexCompletionDate = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_COMPLETION_DATE);
+ int indexReasonDetailed = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_REASON_DETAILED);
return new DownloadStatus(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile),
cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, false, true,
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
index 25df7313f..bb5ea4df6 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedCursorMapper.java
@@ -19,26 +19,26 @@ public abstract class FeedCursorMapper {
*/
@NonNull
public static Feed convert(@NonNull Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
- int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
- int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
- int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
- int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
- int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR);
- int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE);
- int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE);
- int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER);
- int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
- int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
- int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
- int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED);
- int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK);
- int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE);
- int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER);
- int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED);
- int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_FEED_ID);
+ int indexLastUpdate = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LASTUPDATE);
+ int indexTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_TITLE);
+ int indexCustomTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_CUSTOM_TITLE);
+ int indexLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LINK);
+ int indexDescription = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DESCRIPTION);
+ int indexPaymentLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexAuthor = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTHOR);
+ int indexLanguage = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LANGUAGE);
+ int indexType = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_TYPE);
+ int indexFeedIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_IDENTIFIER);
+ int indexFileUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOADED);
+ int indexIsPaged = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IS_PAGED);
+ int indexNextPageLink = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_NEXT_PAGE_LINK);
+ int indexHide = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HIDE);
+ int indexSortOrder = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_SORT_ORDER);
+ int indexLastUpdateFailed = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_LAST_UPDATE_FAILED);
+ int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
Feed feed = new Feed(
cursor.getLong(indexId),
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
index 9fc70a2d7..289bcbab8 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedPreferencesCursorMapper.java
@@ -20,21 +20,21 @@ public abstract class FeedPreferencesCursorMapper {
*/
@NonNull
public static FeedPreferences convert(@NonNull Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED);
- int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED);
- int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
- int indexVolumeAdaption = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION);
- int indexUsername = cursor.getColumnIndex(PodDBAdapter.KEY_USERNAME);
- int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD);
- int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER);
- int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER);
- int indexMinimalDurationFilter = cursor.getColumnIndex(PodDBAdapter.KEY_MINIMAL_DURATION_FILTER);
- int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED);
- int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO);
- int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING);
- int indexEpisodeNotification = cursor.getColumnIndex(PodDBAdapter.KEY_EPISODE_NOTIFICATION);
- int indexTags = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_TAGS);
+ int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_FEED_ID);
+ int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED);
+ int indexAutoRefresh = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_KEEP_UPDATED);
+ int indexAutoDeleteAction = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DELETE_ACTION);
+ int indexVolumeAdaption = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION);
+ int indexUsername = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_USERNAME);
+ int indexPassword = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PASSWORD);
+ int indexIncludeFilter = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_INCLUDE_FILTER);
+ int indexExcludeFilter = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_EXCLUDE_FILTER);
+ int indexMinimalDurationFilter = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_MINIMAL_DURATION_FILTER);
+ int indexFeedPlaybackSpeed = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED);
+ int indexAutoSkipIntro = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_SKIP_INTRO);
+ int indexAutoSkipEnding = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_SKIP_ENDING);
+ int indexEpisodeNotification = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_EPISODE_NOTIFICATION);
+ int indexTags = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEED_TAGS);
long feedId = cursor.getLong(indexId);
boolean autoDownload = cursor.getInt(indexAutoDownload) > 0;
diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml
index 35ccc9677..a19a60892 100644
--- a/ui/i18n/src/main/res/values/strings.xml
+++ b/ui/i18n/src/main/res/values/strings.xml
@@ -10,6 +10,7 @@
<string name="statistics_label">Statistics</string>
<string name="add_feed_label">Add Podcast</string>
<string name="episodes_label">Episodes</string>
+ <string name="home_label">Home</string>
<string name="queue_label">Queue</string>
<string name="inbox_label">Inbox</string>
<string name="favorite_episodes_label">Favorites</string>
@@ -50,6 +51,17 @@
<string name="statistics_counting_range">Played between %1$s and %2$s</string>
<string name="statistics_counting_total">Played in total</string>
+ <!-- Home fragment -->
+ <string name="home_surprise_title">Get surprised</string>
+ <string name="home_classics_title">Check your classics</string>
+ <string name="home_continue_title">Continue listening</string>
+ <string name="home_new_title">Review the new</string>
+ <string name="home_downloads_title">Manage downloads</string>
+ <string name="home_welcome_title">Welcome to AntennaPod!</string>
+ <string name="home_welcome_text">You are not subscribed to any podcasts yet. Open the side menu to add a podcast.</string>
+ <string name="navigate_arrows">%s ยป</string>
+ <string name="configure_home">Configure Home Screen</string>
+
<!-- Download Statistics fragment -->
<string name="total_size_downloaded_podcasts">Total size of episodes on the device</string>
diff --git a/ui/png-icons/src/main/res/drawable/ic_widget_skip.xml b/ui/png-icons/src/main/res/drawable/ic_widget_skip.xml
index 664c96e49..fe8a1ed31 100644
--- a/ui/png-icons/src/main/res/drawable/ic_widget_skip.xml
+++ b/ui/png-icons/src/main/res/drawable/ic_widget_skip.xml
@@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="48dp"
+ android:height="48dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path android:fillColor="#ffffff" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java
index e3251a96b..2116a17a4 100644
--- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java
@@ -85,9 +85,7 @@ public class YearStatisticsListAdapter extends RecyclerView.Adapter<RecyclerView
item.year = lastDataPoint / 12;
item.month = lastDataPoint % 12 + 1;
statisticsData.add(item); // Compensate for months without playback
- System.out.println("aaaaa extra:" + item.month + "/" + item.year);
}
- System.out.println("aaaaa add:" + statistic.month + "/" + statistic.year);
statisticsData.add(statistic);
lastDataPoint = (statistic.month - 1) + statistic.year * 12;
}