diff options
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; } |