diff options
Diffstat (limited to 'app/src/main/java')
101 files changed, 2564 insertions, 2293 deletions
diff --git a/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java index 9ed4897d2..d9b912634 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/ViewPagerBottomSheetBehavior.java @@ -53,6 +53,9 @@ public class ViewPagerBottomSheetBehavior<V extends View> extends BottomSheetBeh } public void updateScrollingChild() { + if (viewRef == null) { + return; + } final View scrollingChild = findScrollingChild(viewRef.get()); nestedScrollingChildRef = new WeakReference<>(scrollingChild); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java index 721291597..50794ba5b 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/BugReportActivity.java @@ -99,7 +99,10 @@ public class BugReportActivity extends AppCompatActivity { private void exportLog() { try { File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt"); - filename.createNewFile(); + boolean success = filename.createNewFile(); + if (!success) { + throw new IOException("Unable to create output file"); + } String cmd = "logcat -d -f " + filename.getAbsolutePath(); Runtime.getRuntime().exec(cmd); //share file diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java index 0f1d38db6..92a0909d6 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java @@ -4,8 +4,8 @@ import android.os.Bundle; import android.text.TextUtils; import androidx.appcompat.app.AppCompatActivity; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.storage.DBReader; 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 e18002903..f07ad6ad5 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -20,6 +20,7 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -32,16 +33,23 @@ import androidx.fragment.app.Fragment; 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 org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.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.StorageUtils; -import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; @@ -57,12 +65,8 @@ import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.preferences.PreferenceUpgrader; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.Validate; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; /** * The activity that is shown when the user launches the app. @@ -184,6 +188,11 @@ public class MainActivity extends CastEnabledActivity { if (audioPlayer == null) { return; } + + if (slideOffset == 0.0f) { //STATE_COLLAPSED + audioPlayer.scrollToPage(AudioPlayerFragment.POS_COVER); + } + float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; audioPlayer.getExternalPlayerHolder().setAlpha(1 - condensedSlideOffset); audioPlayer.getExternalPlayerHolder().setVisibility( @@ -193,7 +202,9 @@ public class MainActivity extends CastEnabledActivity { public void setupToolbarToggle(@NonNull Toolbar toolbar, boolean displayUpArrow) { if (drawerLayout != null) { // Tablet layout does not have a drawer - drawerLayout.removeDrawerListener(drawerToggle); + if (drawerToggle != null) { + drawerLayout.removeDrawerListener(drawerToggle); + } drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.drawer_open, R.string.drawer_close); drawerLayout.addDrawerListener(drawerToggle); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java deleted file mode 100644 index 56a66ba93..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ /dev/null @@ -1,665 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.annotation.TargetApi; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.PixelFormat; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageButton; -import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; -import android.widget.TextView; - -import com.bumptech.glide.Glide; - -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.text.NumberFormat; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.cardview.widget.CardView; -import androidx.core.app.ActivityOptionsCompat; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.event.PlaybackPositionEvent; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.TimeSpeedConverter; -import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.core.util.playback.MediaPlayerError; -import de.danoeh.antennapod.core.util.playback.Playable; -import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.dialog.PlaybackControlsDialog; -import de.danoeh.antennapod.dialog.ShareDialog; -import de.danoeh.antennapod.dialog.SkipPreferenceDialog; -import de.danoeh.antennapod.dialog.SleepTimerDialog; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Provides general features which are both needed for playing audio and video - * files. - */ -public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener { - private static final String TAG = "MediaplayerActivity"; - private static final String PREFS = "MediaPlayerActivityPreferences"; - - PlaybackController controller; - - private TextView txtvPosition; - private TextView txtvLength; - SeekBar sbPosition; - private ImageButton butRev; - private TextView txtvRev; - private ImageButton butPlay; - private ImageButton butFF; - private TextView txtvFF; - private ImageButton butSkip; - private CardView cardViewSeek; - private TextView txtvSeek; - - private boolean showTimeLeft = false; - - private boolean isFavorite = false; - - private Disposable disposable; - - private PlaybackController newPlaybackController() { - return new PlaybackController(this) { - @Override - public void onPositionObserverUpdate() { - MediaplayerActivity.this.onPositionObserverUpdate(); - } - - @Override - public void onBufferStart() { - MediaplayerActivity.this.onBufferStart(); - } - - @Override - public void onBufferEnd() { - MediaplayerActivity.this.onBufferEnd(); - } - - @Override - public void onBufferUpdate(float progress) { - MediaplayerActivity.this.onBufferUpdate(progress); - } - - @Override - public void handleError(int code) { - MediaplayerActivity.this.handleError(code); - } - - @Override - public void onReloadNotification(int code) { - MediaplayerActivity.this.onReloadNotification(code); - } - - @Override - public void onSleepTimerUpdate() { - supportInvalidateOptionsMenu(); - } - - @Override - public ImageButton getPlayButton() { - return butPlay; - } - - @Override - public void loadMediaInfo() { - MediaplayerActivity.this.loadMediaInfo(); - } - - @Override - public void onAwaitingVideoSurface() { - MediaplayerActivity.this.onAwaitingVideoSurface(); - } - - @Override - public void onShutdownNotification() { - finish(); - } - - @Override - public void onPlaybackEnd() { - finish(); - } - - @Override - public void onPlaybackSpeedChange() { - MediaplayerActivity.this.onPlaybackSpeedChange(); - } - - @Override - protected void setScreenOn(boolean enable) { - super.setScreenOn(enable); - MediaplayerActivity.this.setScreenOn(enable); - } - - @Override - public void onSetSpeedAbilityChanged() { - MediaplayerActivity.this.onSetSpeedAbilityChanged(); - } - }; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackPositionEvent event) { - onPositionObserverUpdate(); - } - - private void onSetSpeedAbilityChanged() { - Log.d(TAG, "onSetSpeedAbilityChanged()"); - updatePlaybackSpeedButton(); - } - - private void onPlaybackSpeedChange() { - updatePlaybackSpeedButtonText(); - } - - void chooseTheme() { - setTheme(UserPreferences.getTheme()); - } - - void setScreenOn(boolean enable) { - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - chooseTheme(); - super.onCreate(savedInstanceState); - - Log.d(TAG, "onCreate()"); - StorageUtils.checkStorageAvailability(this); - - getWindow().setFormat(PixelFormat.TRANSPARENT); - setupGUI(); - } - - @Override - protected void onPause() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - if (controller != null) { - controller.reinitServiceIfPaused(); - controller.pause(); - } - } - super.onPause(); - } - - /** - * Should be used to switch to another player activity if the mime type is - * not the correct one for the current activity. - */ - protected abstract void onReloadNotification(int notificationCode); - - /** - * Should be used to inform the user that the PlaybackService is currently - * buffering. - */ - protected void onBufferStart() { - - } - - /** - * Should be used to hide the view that was showing the 'buffering'-message. - */ - protected void onBufferEnd() { - - } - - private void onBufferUpdate(float progress) { - if (sbPosition != null) { - sbPosition.setSecondaryProgress((int) (progress * sbPosition.getMax())); - } - } - - @Override - protected void onStart() { - super.onStart(); - controller = newPlaybackController(); - controller.init(); - loadMediaInfo(); - onPositionObserverUpdate(); - EventBus.getDefault().register(this); - } - - @Override - protected void onStop() { - Log.d(TAG, "onStop()"); - if (controller != null) { - controller.release(); - controller = null; // prevent leak - } - if (disposable != null) { - disposable.dispose(); - } - EventBus.getDefault().unregister(this); - super.onStop(); - } - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - Glide.get(this).trimMemory(level); - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - Glide.get(this).clearMemory(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - requestCastButton(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.mediaplayer, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - if (controller == null) { - return false; - } - Playable media = controller.getMedia(); - boolean isFeedMedia = (media instanceof FeedMedia); - - menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed - - boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null ); - menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); - - boolean isItemAndHasLink = isFeedMedia && - ShareUtils.hasLinkToShare(((FeedMedia) media).getItem()); - - boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownload_url() != null; - - menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink); - - menu.findItem(R.id.add_to_favorites_item).setVisible(false); - menu.findItem(R.id.remove_from_favorites_item).setVisible(false); - if (isFeedMedia) { - menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite); - menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite); - } - - menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); - menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (controller == null) { - return false; - } - Playable media = controller.getMedia(); - if (item.getItemId() == android.R.id.home) { - Intent intent = new Intent(MediaplayerActivity.this, - MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK); - - View cover = findViewById(R.id.imgvCover); - if (cover != null) { - ActivityOptionsCompat options = ActivityOptionsCompat - .makeSceneTransitionAnimation(MediaplayerActivity.this, cover, "coverTransition"); - startActivity(intent, options.toBundle()); - } else { - startActivity(intent); - } - finish(); - return true; - } else { - if (media != null) { - final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem - switch (item.getItemId()) { - case R.id.add_to_favorites_item: - if (feedItem != null) { - DBWriter.addFavoriteItem(feedItem); - isFavorite = true; - invalidateOptionsMenu(); - } - break; - case R.id.remove_from_favorites_item: - if (feedItem != null) { - DBWriter.removeFavoriteItem(feedItem); - isFavorite = false; - invalidateOptionsMenu(); - } - break; - case R.id.disable_sleeptimer_item: // Fall-through - case R.id.set_sleeptimer_item: - new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog"); - break; - case R.id.audio_controls: - PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); - dialog.show(getSupportFragmentManager(), "playback_controls"); - break; - case R.id.open_feed_item: - if (feedItem != null) { - Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId()); - startActivity(intent); - } - break; - case R.id.visit_website_item: - IntentUtils.openInBrowser(MediaplayerActivity.this, getWebsiteLinkWithFallback(media)); - break; - case R.id.share_item: - if (feedItem != null) { - ShareDialog shareDialog = ShareDialog.newInstance(feedItem); - shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog"); - } - break; - default: - return false; - } - return true; - } else { - return false; - } - } - } - - private static String getWebsiteLinkWithFallback(Playable media) { - if (media == null) { - return null; - } else if (StringUtils.isNotBlank(media.getWebsiteLink())) { - return media.getWebsiteLink(); - } else if (media instanceof FeedMedia) { - return FeedItemUtil.getLinkWithFallback(((FeedMedia)media).getItem()); - } - return null; - } - - @Override - protected void onResume() { - super.onResume(); - Log.d(TAG, "onResume()"); - StorageUtils.checkStorageAvailability(this); - } - - /** - * Called by 'handleStatus()' when the PlaybackService is waiting for - * a video surface. - */ - protected abstract void onAwaitingVideoSurface(); - - void onPositionObserverUpdate() { - if (controller == null || txtvPosition == null || txtvLength == null) { - return; - } - - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int currentPosition = converter.convert(controller.getPosition()); - int duration = converter.convert(controller.getDuration()); - int remainingTime = converter.convert( - controller.getDuration() - controller.getPosition()); - Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == PlaybackService.INVALID_TIME || - duration == PlaybackService.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time"); - return; - } - txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); - if (showTimeLeft) { - txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime)); - } else { - txtvLength.setText(Converter.getDurationStringLong(duration)); - } - updateProgressbarPosition(currentPosition, duration); - } - - private void updateProgressbarPosition(int position, int duration) { - Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")"); - if(sbPosition == null) { - return; - } - float progress = ((float) position) / duration; - sbPosition.setProgress((int) (progress * sbPosition.getMax())); - } - - /** - * Load information about the media that is going to be played or currently - * being played. This method will be called when the activity is connected - * to the PlaybackService to ensure that the activity has the right - * FeedMedia object. - */ - void loadMediaInfo() { - Log.d(TAG, "loadMediaInfo()"); - if (controller == null || controller.getMedia() == null) { - return; - } - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - onPositionObserverUpdate(); - checkFavorite(); - updatePlaybackSpeedButton(); - } - - void updatePlaybackSpeedButton() { - // Only meaningful on AudioplayerActivity, where it is overridden. - } - - void updatePlaybackSpeedButtonText() { - // Only meaningful on AudioplayerActivity, where it is overridden. - } - - void setupGUI() { - setContentView(getContentViewResourceId()); - sbPosition = findViewById(R.id.sbPosition); - txtvPosition = findViewById(R.id.txtvPosition); - cardViewSeek = findViewById(R.id.cardViewSeek); - txtvSeek = findViewById(R.id.txtvSeek); - - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = UserPreferences.shouldShowRemainingTime(); - Log.d("timeleft", showTimeLeft ? "true" : "false"); - txtvLength = findViewById(R.id.txtvLength); - if (txtvLength != null) { - txtvLength.setOnClickListener(v -> { - showTimeLeft = !showTimeLeft; - Playable media = controller.getMedia(); - if (media == null) { - return; - } - - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - String length; - if (showTimeLeft) { - int remainingTime = converter.convert( - media.getDuration() - media.getPosition()); - - length = "-" + Converter.getDurationStringLong(remainingTime); - } else { - int duration = converter.convert(media.getDuration()); - length = Converter.getDurationStringLong(duration); - } - txtvLength.setText(length); - - UserPreferences.setShowRemainTimeSetting(showTimeLeft); - Log.d("timeleft on click", showTimeLeft ? "true" : "false"); - }); - } - - butRev = findViewById(R.id.butRev); - txtvRev = findViewById(R.id.txtvRev); - if (txtvRev != null) { - txtvRev.setText(NumberFormat.getInstance().format(UserPreferences.getRewindSecs())); - } - butPlay = findViewById(R.id.butPlay); - butFF = findViewById(R.id.butFF); - txtvFF = findViewById(R.id.txtvFF); - if (txtvFF != null) { - txtvFF.setText(NumberFormat.getInstance().format(UserPreferences.getFastForwardSecs())); - } - butSkip = findViewById(R.id.butSkip); - - // SEEKBAR SETUP - - sbPosition.setOnSeekBarChangeListener(this); - - // BUTTON SETUP - - if (butRev != null) { - butRev.setOnClickListener(v -> onRewind()); - butRev.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this, - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev); - return true; - }); - } - - butPlay.setOnClickListener(v -> onPlayPause()); - - if (butFF != null) { - butFF.setOnClickListener(v -> onFastForward()); - butFF.setOnLongClickListener(v -> { - SkipPreferenceDialog.showSkipPreference(MediaplayerActivity.this, - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF); - return false; - }); - } - - if (butSkip != null) { - butSkip.setOnClickListener(v -> - IntentUtils.sendLocalBroadcast(MediaplayerActivity.this, PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); - } - } - - void onRewind() { - if (controller == null) { - return; - } - int curr = controller.getPosition(); - controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); - } - - void onPlayPause() { - if(controller == null) { - return; - } - controller.init(); - controller.playPause(); - } - - void onFastForward() { - if (controller == null) { - return; - } - int curr = controller.getPosition(); - controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); - } - - protected abstract int getContentViewResourceId(); - - private void handleError(int errorCode) { - final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this); - errorDialog.setTitle(R.string.error_label); - errorDialog.setMessage(MediaPlayerError.getErrorString(this, errorCode)); - errorDialog.setNeutralButton("OK", - (dialog, which) -> { - dialog.dismiss(); - finish(); - } - ); - errorDialog.create().show(); - } - - private float prog; - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (controller == null || txtvLength == null) { - return; - } - if (fromUser) { - prog = progress / ((float) seekBar.getMax()); - TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * controller.getDuration())); - txtvSeek.setText(Converter.getDurationStringLong(position)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - cardViewSeek.setScaleX(.8f); - cardViewSeek.setScaleY(.8f); - cardViewSeek.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(200) - .start(); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (controller != null) { - controller.seekTo((int) (prog * controller.getDuration())); - } - cardViewSeek.setScaleX(1f); - cardViewSeek.setScaleY(1f); - cardViewSeek.animate() - .setInterpolator(new FastOutSlowInInterpolator()) - .alpha(0f).scaleX(.8f).scaleY(.8f) - .setDuration(200) - .start(); - } - - private void checkFavorite() { - FeedItem feedItem = getFeedItem(controller.getMedia()); - if (feedItem == null) { - return; - } - if (disposable != null) { - disposable.dispose(); - } - disposable = Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - item -> { - boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE); - if (isFavorite != isFav) { - isFavorite = isFav; - invalidateOptionsMenu(); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - - @Nullable - private static FeedItem getFeedItem(@Nullable Playable playable) { - if (playable instanceof FeedMedia) { - return ((FeedMedia) playable).getItem(); - } else { - return null; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index c6f2cc84b..575e94f8c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -15,10 +15,12 @@ import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -31,10 +33,6 @@ import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedPreferences; -import de.danoeh.antennapod.core.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -54,18 +52,22 @@ import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeExceptio import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URLChecker; -import de.danoeh.antennapod.core.util.playback.RemoteMedia; import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding; import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.discovery.PodcastSearcherRegistry; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.model.playback.RemoteMedia; +import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableMaybeObserver; import io.reactivex.schedulers.Schedulers; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; @@ -256,7 +258,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity { url = URLChecker.prepareURL(url); feed = new Feed(url, null); if (username != null && password != null) { - feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)); + feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, + VolumeAdaptionSetting.OFF, username, password)); } String fileUrl = new File(getExternalCacheDir(), FileNameGenerator.generateFileName(feed.getDownload_url())).toString(); @@ -320,33 +323,45 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } Log.d(TAG, "Parsing feed"); - parser = Observable.fromCallable(this::doParseFeed) + parser = Maybe.fromCallable(this::doParseFeed) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - optionalResult -> { - if (optionalResult.isPresent()) { - FeedHandlerResult result = optionalResult.get(); - beforeShowFeedInformation(result.feed); - showFeedInformation(result.feed, result.alternateFeedUrls); - } - }, error -> { + .subscribeWith(new DisposableMaybeObserver<FeedHandlerResult>() { + @Override + public void onSuccess(@NonNull FeedHandlerResult result) { + showFeedInformation(result.feed, result.alternateFeedUrls); + } + + @Override + public void onComplete() { + // Ignore null result: We showed the discovery dialog. + } + + @Override + public void onError(@NonNull Throwable error) { showErrorDialog(error.getMessage(), ""); Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); - }); + } + }); } - @NonNull - private Optional<FeedHandlerResult> doParseFeed() throws Exception { + /** + * Try to parse the feed. + * @return The FeedHandlerResult if successful. + * Null if unsuccessful but we started another attempt. + * @throws Exception If unsuccessful but we do not know a resolution. + */ + @Nullable + private FeedHandlerResult doParseFeed() throws Exception { FeedHandler handler = new FeedHandler(); try { - return Optional.of(handler.parseFeed(feed)); + return handler.parseFeed(feed); } catch (UnsupportedFeedtypeException e) { Log.d(TAG, "Unsupported feed type detected"); if ("html".equalsIgnoreCase(e.getRootElement())) { boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); if (dialogShown) { - return Optional.empty(); + return null; // Should not display an error message } else { throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)); } @@ -363,23 +378,6 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } /** - * Called after the feed has been downloaded and parsed and before showFeedInformation is called. - * This method is executed on a background thread - */ - private void beforeShowFeedInformation(Feed feed) { - Log.d(TAG, "Removing HTML from feed description"); - - feed.setDescription(HtmlToPlainText.getPlainText(feed.getDescription())); - - Log.d(TAG, "Removing HTML from shownotes"); - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - item.setDescription(HtmlToPlainText.getPlainText(item.getDescription())); - } - } - } - - /** * Called when feed parsed successfully. * This method is executed on the GUI thread. */ @@ -422,7 +420,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { viewBinding.titleLabel.setText(feed.getTitle()); viewBinding.authorLabel.setText(feed.getAuthor()); - description.setText(feed.getDescription()); + description.setText(HtmlToPlainText.getPlainText(feed.getDescription())); viewBinding.subscribeButton.setOnClickListener(v -> { if (feedInFeedlist(feed)) { @@ -479,8 +477,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity { for (String url : alternateFeedUrls.keySet()) { alternateUrlsTitleList.add(alternateFeedUrls.get(url)); } - ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, alternateUrlsTitleList); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + R.layout.alternate_urls_item, alternateUrlsTitleList) { + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + // reusing the old view causes a visual bug on Android <= 10 + return super.getDropDownView(position, null, parent); + } + }; + + adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item); viewBinding.alternateUrlsSpinner.setAdapter(adapter); viewBinding.alternateUrlsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index 6e526911b..cd72e34e8 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -1,19 +1,23 @@ package de.danoeh.antennapod.activity; +import android.content.Intent; +import android.os.Build; import android.os.Bundle; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; + +import android.provider.Settings; import android.view.Menu; import android.view.MenuItem; -import android.view.ViewGroup; -import android.widget.FrameLayout; import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.databinding.SettingsActivityBinding; import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.GpodderPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment; @@ -30,6 +34,7 @@ import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragmen */ public class PreferenceActivity extends AppCompatActivity implements SearchPreferenceResultListener { private static final String FRAGMENT_TAG = "tag_preferences"; + public static final String OPEN_AUTO_DOWNLOAD_SETTINGS = "OpenAutoDownloadSettings"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -41,17 +46,18 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe ab.setDisplayHomeAsUpEnabled(true); } - FrameLayout root = new FrameLayout(this); - root.setId(R.id.content); - root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - setContentView(root); + final SettingsActivityBinding binding = SettingsActivityBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG) == null) { getSupportFragmentManager().beginTransaction() - .replace(R.id.content, new MainPreferencesFragment(), FRAGMENT_TAG) + .replace(R.id.settingsContainer, new MainPreferencesFragment(), FRAGMENT_TAG) .commit(); } + Intent intent = getIntent(); + if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) { + openScreen(R.xml.preferences_autodownload); + } } private PreferenceFragmentCompat getPreferenceScreen(int screen) { @@ -95,6 +101,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe return R.string.gpodnet_main_label; case R.xml.preferences_notifications: return R.string.notification_pref_fragment; + case R.xml.feed_settings: + return R.string.feed_settings_label; default: return R.string.settings_label; } @@ -102,8 +110,17 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe public PreferenceFragmentCompat openScreen(int screen) { PreferenceFragmentCompat fragment = getPreferenceScreen(screen); - getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment) - .addToBackStack(getString(getTitleOfPage(screen))).commit(); + if (screen == R.xml.preferences_notifications && Build.VERSION.SDK_INT >= 26) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + startActivity(intent); + } else { + getSupportFragmentManager().beginTransaction().replace(R.id.settingsContainer, fragment) + .addToBackStack(getString(getTitleOfPage(screen))).commit(); + } + + return fragment; } @@ -128,7 +145,18 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe @Override public void onSearchResultClicked(SearchPreferenceResult result) { - PreferenceFragmentCompat fragment = openScreen(result.getResourceFile()); - result.highlight(fragment); + int screen = result.getResourceFile(); + if (screen == R.xml.feed_settings) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.feed_settings_label); + builder.setMessage(R.string.pref_feed_settings_dialog_msg); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (screen == R.xml.preferences_notifications) { + openScreen(screen); + } else { + PreferenceFragmentCompat fragment = openScreen(result.getResourceFile()); + result.highlight(fragment); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 15d0bec4a..d436acf0d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -1,53 +1,79 @@ package de.danoeh.antennapod.activity; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Intent; +import android.graphics.PixelFormat; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.animation.AlphaAnimation; -import android.view.animation.AnimationSet; -import android.view.animation.ScaleAnimation; -import android.widget.EditText; -import android.widget.ImageView; - -import androidx.core.view.WindowCompat; -import androidx.appcompat.app.ActionBar; +import android.os.Looper; import android.util.Log; import android.util.Pair; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; import android.view.WindowManager; +import android.view.animation.AlphaAnimation; import android.view.animation.Animation; +import android.view.animation.AnimationSet; import android.view.animation.AnimationUtils; +import android.view.animation.ScaleAnimation; +import android.widget.EditText; import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.ProgressBar; import android.widget.SeekBar; - -import java.lang.ref.WeakReference; -import java.util.concurrent.atomic.AtomicBoolean; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.view.WindowCompat; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import com.bumptech.glide.Glide; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.databinding.VideoplayerActivityBinding; +import de.danoeh.antennapod.dialog.PlaybackControlsDialog; +import de.danoeh.antennapod.dialog.ShareDialog; +import de.danoeh.antennapod.dialog.SkipPreferenceDialog; +import de.danoeh.antennapod.dialog.SleepTimerDialog; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.view.AspectRatioVideoView; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Activity for playing video files. */ -public class VideoplayerActivity extends MediaplayerActivity { +public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.OnSeekBarChangeListener { private static final String TAG = "VideoplayerActivity"; /** @@ -57,36 +83,39 @@ public class VideoplayerActivity extends MediaplayerActivity { private boolean videoSurfaceCreated = false; private boolean destroyingDueToReload = false; private long lastScreenTap = 0; - - private VideoControlsHider videoControlsHider = new VideoControlsHider(this); - - private final AtomicBoolean isSetup = new AtomicBoolean(false); - - private LinearLayout controls; - private LinearLayout videoOverlay; - private AspectRatioVideoView videoview; - private ProgressBar progressIndicator; - private FrameLayout videoframe; - private ImageView skipAnimationView; - - @Override - protected void chooseTheme() { - setTheme(R.style.Theme_AntennaPod_VideoPlayer); - } + private Handler videoControlsHider = new Handler(Looper.getMainLooper()); + private VideoplayerActivityBinding viewBinding; + private PlaybackController controller; + private boolean showTimeLeft = false; + private boolean isFavorite = false; + private Disposable disposable; + private float prog; @SuppressLint("AppCompatMethod") @Override protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); - supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); // has to be called before setting layout content + // has to be called before setting layout content + supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); + setTheme(R.style.Theme_AntennaPod_VideoPlayer); super.onCreate(savedInstanceState); + + Log.d(TAG, "onCreate()"); + StorageUtils.checkStorageAvailability(this); + + getWindow().setFormat(PixelFormat.TRANSPARENT); + viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this)); + setContentView(viewBinding.getRoot()); + setupView(); getSupportActionBar().setBackgroundDrawable(new ColorDrawable(0x80000000)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); } @Override protected void onResume() { super.onResume(); + StorageUtils.checkStorageAvailability(this); if (PlaybackService.isCasting()) { Intent intent = PlaybackService.getPlayerActivityIntent(this); if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { @@ -99,11 +128,20 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onStop() { + if (controller != null) { + controller.release(); + controller = null; // prevent leak + } + if (disposable != null) { + disposable.dispose(); + } + EventBus.getDefault().unregister(this); super.onStop(); if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - videoControlsHider.stop(); + videoControlsHider.removeCallbacks(hideVideoControls); } - progressIndicator.setVisibility(View.GONE); // Controller released; we will not receive buffering updates + // Controller released; we will not receive buffering updates + viewBinding.progressBar.setVisibility(View.GONE); } @Override @@ -115,6 +153,16 @@ public class VideoplayerActivity extends MediaplayerActivity { } @Override + protected void onStart() { + super.onStart(); + controller = newPlaybackController(); + controller.init(); + loadMediaInfo(); + onPositionObserverUpdate(); + EventBus.getDefault().register(this); + } + + @Override protected void onPause() { if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) { @@ -124,16 +172,103 @@ public class VideoplayerActivity extends MediaplayerActivity { super.onPause(); } + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override - protected void onDestroy() { - videoControlsHider.stop(); - videoControlsHider = null; - super.onDestroy(); + public void onTrimMemory(int level) { + super.onTrimMemory(level); + Glide.get(this).trimMemory(level); } @Override + public void onLowMemory() { + super.onLowMemory(); + Glide.get(this).clearMemory(); + } + + private PlaybackController newPlaybackController() { + return new PlaybackController(this) { + @Override + public void onPositionObserverUpdate() { + VideoplayerActivity.this.onPositionObserverUpdate(); + } + + @Override + public void onBufferStart() { + viewBinding.progressBar.setVisibility(View.VISIBLE); + } + + @Override + public void onBufferEnd() { + viewBinding.progressBar.setVisibility(View.INVISIBLE); + } + + @Override + public void onBufferUpdate(float progress) { + viewBinding.sbPosition.setSecondaryProgress((int) (progress * viewBinding.sbPosition.getMax())); + } + + @Override + public void handleError(int code) { + final AlertDialog.Builder errorDialog = new AlertDialog.Builder(VideoplayerActivity.this); + errorDialog.setTitle(R.string.error_label); + errorDialog.setMessage(MediaPlayerError.getErrorString(VideoplayerActivity.this, code)); + errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish()); + errorDialog.show(); + } + + @Override + public void onReloadNotification(int code) { + VideoplayerActivity.this.onReloadNotification(code); + } + + @Override + public void onSleepTimerUpdate() { + supportInvalidateOptionsMenu(); + } + + @Override + protected void updatePlayButtonShowsPlay(boolean showPlay) { + viewBinding.playButton.setIsShowPlay(showPlay); + } + + @Override + public void loadMediaInfo() { + VideoplayerActivity.this.loadMediaInfo(); + } + + @Override + public void onAwaitingVideoSurface() { + setupVideoAspectRatio(); + if (videoSurfaceCreated && controller != null) { + Log.d(TAG, "Videosurface already created, setting videosurface now"); + controller.setVideoSurface(viewBinding.videoView.getHolder()); + } + } + + @Override + public void onPlaybackEnd() { + finish(); + } + + @Override + protected void setScreenOn(boolean enable) { + if (enable) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + }; + } + protected void loadMediaInfo() { - super.loadMediaInfo(); + Log.d(TAG, "loadMediaInfo()"); + if (controller == null || controller.getMedia() == null) { + return; + } + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + onPositionObserverUpdate(); + checkFavorite(); Playable media = controller.getMedia(); if (media != null) { getSupportActionBar().setSubtitle(media.getEpisodeTitle()); @@ -141,75 +276,103 @@ public class VideoplayerActivity extends MediaplayerActivity { } } - @Override - protected void setupGUI() { - if (isSetup.getAndSet(true)) { - return; - } - super.setupGUI(); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - controls = findViewById(R.id.controls); - videoOverlay = findViewById(R.id.overlay); - videoview = findViewById(R.id.videoview); - videoframe = findViewById(R.id.videoframe); - progressIndicator = findViewById(R.id.progressIndicator); - skipAnimationView = findViewById(R.id.skip_animation); - videoview.getHolder().addCallback(surfaceHolderCallback); - videoframe.setOnTouchListener(onVideoviewTouched); - videoOverlay.setOnTouchListener((view, motionEvent) -> true); // To suppress touches directly below the slider - videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - videoOverlay.setFitsSystemWindows(true); + protected void setupView() { + showTimeLeft = UserPreferences.shouldShowRemainingTime(); + Log.d("timeleft", showTimeLeft ? "true" : "false"); + viewBinding.durationLabel.setOnClickListener(v -> { + showTimeLeft = !showTimeLeft; + Playable media = controller.getMedia(); + if (media == null) { + return; + } + + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + String length; + if (showTimeLeft) { + int remainingTime = converter.convert(media.getDuration() - media.getPosition()); + length = "-" + Converter.getDurationStringLong(remainingTime); + } else { + int duration = converter.convert(media.getDuration()); + length = Converter.getDurationStringLong(duration); + } + viewBinding.durationLabel.setText(length); + + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + Log.d("timeleft on click", showTimeLeft ? "true" : "false"); + }); + + viewBinding.sbPosition.setOnSeekBarChangeListener(this); + viewBinding.rewindButton.setOnClickListener(v -> onRewind()); + viewBinding.rewindButton.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null); + return true; + }); + viewBinding.playButton.setIsVideoScreen(true); + viewBinding.playButton.setOnClickListener(v -> onPlayPause()); + viewBinding.fastForwardButton.setOnClickListener(v -> onFastForward()); + viewBinding.fastForwardButton.setOnLongClickListener(v -> { + SkipPreferenceDialog.showSkipPreference(VideoplayerActivity.this, + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null); + return false; + }); + // To suppress touches directly below the slider + viewBinding.bottomControlsContainer.setOnTouchListener((view, motionEvent) -> true); + viewBinding.bottomControlsContainer.setFitsSystemWindows(true); + viewBinding.videoView.getHolder().addCallback(surfaceHolderCallback); + viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); setupVideoControlsToggler(); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); - videoframe.getViewTreeObserver().addOnGlobalLayoutListener(() -> - videoview.setAvailableSize(videoframe.getWidth(), videoframe.getHeight())); + viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched); + viewBinding.videoPlayerContainer.getViewTreeObserver().addOnGlobalLayoutListener(() -> + viewBinding.videoView.setAvailableSize( + viewBinding.videoPlayerContainer.getWidth(), viewBinding.videoPlayerContainer.getHeight())); } - @Override - protected void onAwaitingVideoSurface() { - setupVideoAspectRatio(); - if (videoSurfaceCreated && controller != null) { - Log.d(TAG, "Videosurface already created, setting videosurface now"); - controller.setVideoSurface(videoview.getHolder()); + private final Runnable hideVideoControls = () -> { + if (videoControlsShowing) { + Log.d(TAG, "Hiding video controls"); + getSupportActionBar().hide(); + hideVideoControls(true); + videoControlsShowing = false; } - } + }; private final View.OnTouchListener onVideoviewTouched = (v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (PictureInPictureUtil.isInPictureInPictureMode(this)) { - return true; - } - videoControlsHider.stop(); + if (event.getAction() != MotionEvent.ACTION_DOWN) { + return false; + } + if (PictureInPictureUtil.isInPictureInPictureMode(this)) { + return true; + } + videoControlsHider.removeCallbacks(hideVideoControls); - if (System.currentTimeMillis() - lastScreenTap < 300) { - if (event.getX() > v.getMeasuredWidth() / 2.0f) { - onFastForward(); - showSkipAnimation(true); - } else { - onRewind(); - showSkipAnimation(false); - } - if (videoControlsShowing) { - getSupportActionBar().hide(); - hideVideoControls(false); - videoControlsShowing = false; - } - return true; + if (System.currentTimeMillis() - lastScreenTap < 300) { + if (event.getX() > v.getMeasuredWidth() / 2.0f) { + onFastForward(); + showSkipAnimation(true); + } else { + onRewind(); + showSkipAnimation(false); } - - toggleVideoControlsVisibility(); if (videoControlsShowing) { - setupVideoControlsToggler(); + getSupportActionBar().hide(); + hideVideoControls(false); + videoControlsShowing = false; } - - lastScreenTap = System.currentTimeMillis(); return true; - } else { - return false; } + + toggleVideoControlsVisibility(); + if (videoControlsShowing) { + setupVideoControlsToggler(); + } + + lastScreenTap = System.currentTimeMillis(); + return true; }; private void showSkipAnimation(boolean isForward) { @@ -220,18 +383,18 @@ public class VideoplayerActivity extends MediaplayerActivity { skipAnimation.setFillAfter(false); skipAnimation.setDuration(800); - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) skipAnimationView.getLayoutParams(); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewBinding.skipAnimationImage.getLayoutParams(); if (isForward) { - skipAnimationView.setImageResource(R.drawable.ic_av_fast_forward_white_80dp); + viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white); params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; } else { - skipAnimationView.setImageResource(R.drawable.ic_av_fast_rewind_white_80dp); + viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white); params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; } - skipAnimationView.setVisibility(View.VISIBLE); - skipAnimationView.setLayoutParams(params); - skipAnimationView.startAnimation(skipAnimation); + viewBinding.skipAnimationImage.setVisibility(View.VISIBLE); + viewBinding.skipAnimationImage.setLayoutParams(params); + viewBinding.skipAnimationImage.startAnimation(skipAnimation); skipAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { @@ -239,7 +402,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override public void onAnimationEnd(Animation animation) { - skipAnimationView.setVisibility(View.GONE); + viewBinding.skipAnimationImage.setVisibility(View.GONE); } @Override @@ -249,10 +412,9 @@ public class VideoplayerActivity extends MediaplayerActivity { }); } - @SuppressLint("NewApi") private void setupVideoControlsToggler() { - videoControlsHider.stop(); - videoControlsHider.start(); + videoControlsHider.removeCallbacks(hideVideoControls); + videoControlsHider.postDelayed(hideVideoControls, 2500); } private void setupVideoAspectRatio() { @@ -260,7 +422,7 @@ public class VideoplayerActivity extends MediaplayerActivity { Pair<Integer, Integer> videoSize = controller.getVideoSize(); if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second); - videoview.setVideoSize(videoSize.first, videoSize.second); + viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second); } else { Log.e(TAG, "Could not determine video size"); } @@ -270,7 +432,7 @@ public class VideoplayerActivity extends MediaplayerActivity { private void toggleVideoControlsVisibility() { if (videoControlsShowing) { getSupportActionBar().hide(); - hideVideoControls(); + hideVideoControls(true); } else { getSupportActionBar().show(); showVideoControls(); @@ -278,29 +440,35 @@ public class VideoplayerActivity extends MediaplayerActivity { videoControlsShowing = !videoControlsShowing; } - @Override - protected void onRewind() { - super.onRewind(); + void onRewind() { + if (controller == null) { + return; + } + int curr = controller.getPosition(); + controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000); setupVideoControlsToggler(); } - @Override - protected void onPlayPause() { - super.onPlayPause(); + void onPlayPause() { + if (controller == null) { + return; + } + controller.playPause(); setupVideoControlsToggler(); } - @Override - protected void onFastForward() { - super.onFastForward(); + void onFastForward() { + if (controller == null) { + return; + } + int curr = controller.getPosition(); + controller.seekTo(curr + UserPreferences.getFastForwardSecs() * 1000); setupVideoControlsToggler(); } - private final SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { holder.setFixedSize(width, height); } @@ -326,8 +494,6 @@ public class VideoplayerActivity extends MediaplayerActivity { } }; - - @Override protected void onReloadNotification(int notificationCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && PictureInPictureUtil.isInPictureInPictureMode(this)) { if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO @@ -344,84 +510,87 @@ public class VideoplayerActivity extends MediaplayerActivity { } } - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - super.onStartTrackingTouch(seekBar); - videoControlsHider.stop(); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - setupVideoControlsToggler(); - } - - @Override - protected void onBufferStart() { - progressIndicator.setVisibility(View.VISIBLE); - } - - @Override - protected void onBufferEnd() { - progressIndicator.setVisibility(View.INVISIBLE); - } - - @SuppressLint("NewApi") private void showVideoControls() { - videoOverlay.setVisibility(View.VISIBLE); - controls.setVisibility(View.VISIBLE); + viewBinding.bottomControlsContainer.setVisibility(View.VISIBLE); + viewBinding.controlsContainer.setVisibility(View.VISIBLE); final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in); if (animation != null) { - videoOverlay.startAnimation(animation); - controls.startAnimation(animation); + viewBinding.bottomControlsContainer.startAnimation(animation); + viewBinding.controlsContainer.startAnimation(animation); } - videoview.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); } - @SuppressLint("NewApi") private void hideVideoControls(boolean showAnimation) { if (showAnimation) { final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out); if (animation != null) { - videoOverlay.startAnimation(animation); - controls.startAnimation(animation); + viewBinding.bottomControlsContainer.startAnimation(animation); + viewBinding.controlsContainer.startAnimation(animation); } } getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - videoOverlay.setFitsSystemWindows(true); + viewBinding.bottomControlsContainer.setFitsSystemWindows(true); - videoOverlay.setVisibility(View.GONE); - controls.setVisibility(View.GONE); + viewBinding.bottomControlsContainer.setVisibility(View.GONE); + viewBinding.controlsContainer.setVisibility(View.GONE); } - private void hideVideoControls() { - hideVideoControls(true); + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + onPositionObserverUpdate(); } - @Override - protected int getContentViewResourceId() { - return R.layout.videoplayer_activity; + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(ServiceEvent event) { + if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) { + finish(); + } } @Override - protected void setScreenOn(boolean enable) { - super.setScreenOn(enable); - if (enable) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + requestCastButton(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mediaplayer, menu); + return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); + if (controller == null) { + return false; + } + Playable media = controller.getMedia(); + boolean isFeedMedia = (media instanceof FeedMedia); + + menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia); // FeedMedia implies it belongs to a Feed + + boolean hasWebsiteLink = getWebsiteLinkWithFallback(media) != null; + menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); + + boolean isItemAndHasLink = isFeedMedia && ShareUtils.hasLinkToShare(((FeedMedia) media).getItem()); + boolean isItemHasDownloadLink = isFeedMedia && ((FeedMedia) media).getDownload_url() != null; + menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink); + + menu.findItem(R.id.add_to_favorites_item).setVisible(false); + menu.findItem(R.id.remove_from_favorites_item).setVisible(false); + if (isFeedMedia) { + menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite); + menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite); + } + + menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive()); + menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive()); + if (PictureInPictureUtil.supportsPictureInPicture(this)) { menu.findItem(R.id.player_go_to_picture_in_picture).setVisible(true); } - menu.findItem(R.id.audio_controls).setIcon(R.drawable.ic_sliders_white); + menu.findItem(R.id.audio_controls).setIcon(R.drawable.ic_sliders); return true; } @@ -431,53 +600,170 @@ public class VideoplayerActivity extends MediaplayerActivity { compatEnterPictureInPicture(); return true; } - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + Intent intent = new Intent(VideoplayerActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + return true; + } + + if (controller == null) { + return false; + } + + Playable media = controller.getMedia(); + if (media == null) { + return false; + } + final @Nullable FeedItem feedItem = getFeedItem(media); // some options option requires FeedItem + if (item.getItemId() == R.id.add_to_favorites_item && feedItem != null) { + DBWriter.addFavoriteItem(feedItem); + isFavorite = true; + invalidateOptionsMenu(); + } else if (item.getItemId() == R.id.remove_from_favorites_item && feedItem != null) { + DBWriter.removeFavoriteItem(feedItem); + isFavorite = false; + invalidateOptionsMenu(); + } else if (item.getItemId() == R.id.disable_sleeptimer_item + || item.getItemId() == R.id.set_sleeptimer_item) { + new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog"); + } else if (item.getItemId() == R.id.audio_controls) { + PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(); + dialog.show(getSupportFragmentManager(), "playback_controls"); + } else if (item.getItemId() == R.id.open_feed_item && feedItem != null) { + Intent intent = MainActivity.getIntentToOpenFeed(this, feedItem.getFeedId()); + startActivity(intent); + } else if (item.getItemId() == R.id.visit_website_item) { + IntentUtils.openInBrowser(VideoplayerActivity.this, getWebsiteLinkWithFallback(media)); + } else if (item.getItemId() == R.id.share_item && feedItem != null) { + ShareDialog shareDialog = ShareDialog.newInstance(feedItem); + shareDialog.show(getSupportFragmentManager(), "ShareEpisodeDialog"); + } else { + return false; + } + return true; } - private void compatEnterPictureInPicture() { - if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - getSupportActionBar().hide(); - hideVideoControls(false); - enterPictureInPictureMode(); + private static String getWebsiteLinkWithFallback(Playable media) { + if (media == null) { + return null; + } else if (StringUtils.isNotBlank(media.getWebsiteLink())) { + return media.getWebsiteLink(); + } else if (media instanceof FeedMedia) { + return FeedItemUtil.getLinkWithFallback(((FeedMedia) media).getItem()); } + return null; } - private static class VideoControlsHider extends Handler { + void onPositionObserverUpdate() { + if (controller == null) { + return; + } - private static final int DELAY = 2500; + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int currentPosition = converter.convert(controller.getPosition()); + int duration = converter.convert(controller.getDuration()); + int remainingTime = converter.convert( + controller.getDuration() - controller.getPosition()); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == PlaybackService.INVALID_TIME + || duration == PlaybackService.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + viewBinding.positionLabel.setText(Converter.getDurationStringLong(currentPosition)); + if (showTimeLeft) { + viewBinding.durationLabel.setText("-" + Converter.getDurationStringLong(remainingTime)); + } else { + viewBinding.durationLabel.setText(Converter.getDurationStringLong(duration)); + } + updateProgressbarPosition(currentPosition, duration); + } - private WeakReference<VideoplayerActivity> activity; + private void updateProgressbarPosition(int position, int duration) { + Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")"); + float progress = ((float) position) / duration; + viewBinding.sbPosition.setProgress((int) (progress * viewBinding.sbPosition.getMax())); + } - VideoControlsHider(VideoplayerActivity activity) { - this.activity = new WeakReference<>(activity); + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (controller == null) { + return; + } + if (fromUser) { + prog = progress / ((float) seekBar.getMax()); + TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); + int position = converter.convert((int) (prog * controller.getDuration())); + viewBinding.seekPositionLabel.setText(Converter.getDurationStringLong(position)); } + } - private final Runnable hideVideoControls = () -> { - VideoplayerActivity vpa = activity != null ? activity.get() : null; - if (vpa == null) { - return; - } - if (vpa.videoControlsShowing) { - Log.d(TAG, "Hiding video controls"); - ActionBar actionBar = vpa.getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); - } - vpa.hideVideoControls(); - vpa.videoControlsShowing = false; - } - }; + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + viewBinding.seekCardView.setScaleX(.8f); + viewBinding.seekCardView.setScaleY(.8f); + viewBinding.seekCardView.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); + videoControlsHider.removeCallbacks(hideVideoControls); + } - public void start() { - this.postDelayed(hideVideoControls, DELAY); + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (controller != null) { + controller.seekTo((int) (prog * controller.getDuration())); } + viewBinding.seekCardView.setScaleX(1f); + viewBinding.seekCardView.setScaleY(1f); + viewBinding.seekCardView.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); + setupVideoControlsToggler(); + } - void stop() { - this.removeCallbacks(hideVideoControls); + private void checkFavorite() { + FeedItem feedItem = getFeedItem(controller.getMedia()); + if (feedItem == null) { + return; + } + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + item -> { + boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE); + if (isFavorite != isFav) { + isFavorite = isFav; + invalidateOptionsMenu(); + } + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + @Nullable + private static FeedItem getFeedItem(@Nullable Playable playable) { + if (playable instanceof FeedMedia) { + return ((FeedMedia) playable).getItem(); + } else { + return null; } - } + private void compatEnterPictureInPicture() { + if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + getSupportActionBar().hide(); + hideVideoControls(false); + enterPictureInPictureMode(); + } + } //Hardware keyboard support @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java index 8380d8626..faa18434e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java @@ -15,13 +15,13 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.ui.common.CircularProgressBar; public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder> { @@ -77,7 +77,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte holder.link.setText(sc.getLink()); holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(context, sc.getLink())); } - holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.av_play)); + holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp); holder.secondaryActionButton.setContentDescription(context.getString(R.string.play_chapter)); holder.secondaryActionButton.setOnClickListener(v -> { if (callback != null) { @@ -92,7 +92,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte progress = Math.max(progress, CircularProgressBar.MINIMUM_PERCENTAGE); progress = Math.min(progress, CircularProgressBar.MAXIMUM_PERCENTAGE); holder.progressBar.setPercentage(progress, position); - holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.av_replay)); + holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay); } else { holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); holder.progressBar.setPercentage(0, null); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java index d782d4ed5..aeaf526be 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java @@ -21,6 +21,7 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.glide.ApGlideSettings; public class CoverLoader { + private int resource = 0; private String uri; private String fallbackUri; private TextView txtvPlaceholder; @@ -37,6 +38,11 @@ public class CoverLoader { return this; } + public CoverLoader withResource(int resource) { + this.resource = resource; + return this; + } + public CoverLoader withFallbackUri(String uri) { fallbackUri = uri; return this; @@ -66,6 +72,12 @@ public class CoverLoader { } public void load() { + if (resource != 0) { + imgvCover.setImageResource(resource); + CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined); + return; + } + RequestOptions options = new RequestOptions() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .fitCenter() @@ -106,15 +118,7 @@ public class CoverLoader { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { - TextView txtvPlaceholder = placeholder.get(); - if (txtvPlaceholder != null) { - if (textAndImageCombined) { - int bgColor = txtvPlaceholder.getContext().getResources().getColor(R.color.feed_text_bg); - txtvPlaceholder.setBackgroundColor(bgColor); - } else { - txtvPlaceholder.setVisibility(View.INVISIBLE); - } - } + setPlaceholderVisibility(placeholder.get(), textAndImageCombined); ImageView ivCover = cover.get(); ivCover.setImageDrawable(resource); } @@ -124,5 +128,16 @@ public class CoverLoader { ImageView ivCover = cover.get(); ivCover.setImageDrawable(placeholder); } + + static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined) { + if (placeholder != null) { + if (textAndImageCombined) { + int bgColor = placeholder.getContext().getResources().getColor(R.color.feed_text_bg); + placeholder.setBackgroundColor(bgColor); + } else { + placeholder.setVisibility(View.INVISIBLE); + } + } + } } }
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 811e1e31b..740636c77 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -2,26 +2,32 @@ package de.danoeh.antennapod.adapter; import android.app.Activity; import android.text.format.DateUtils; -import android.util.Log; +import android.text.format.Formatter; import android.view.View; import android.view.ViewGroup; +import android.util.Log; import android.widget.BaseAdapter; -import android.widget.Toast; +import android.widget.Toast; import androidx.core.content.ContextCompat; - +import androidx.fragment.app.ListFragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder; +import de.danoeh.antennapod.view.viewholder.DownloadLogItemViewHolder; + +import java.util.ArrayList; +import java.util.List; /** * Displays a list of DownloadStatus entries. @@ -30,37 +36,62 @@ public class DownloadLogAdapter extends BaseAdapter { private static final String TAG = "DownloadLogAdapter"; private final Activity context; - private final ItemAccess itemAccess; + private final ListFragment listFragment; + private List<DownloadStatus> downloadLog = new ArrayList<>(); + private List<Downloader> runningDownloads = new ArrayList<>(); - public DownloadLogAdapter(Activity context, ItemAccess itemAccess) { + public DownloadLogAdapter(Activity context, ListFragment listFragment) { super(); - this.itemAccess = itemAccess; this.context = context; + this.listFragment = listFragment; + } + + public void setDownloadLog(List<DownloadStatus> downloadLog) { + this.downloadLog = downloadLog; + notifyDataSetChanged(); + } + + public void setRunningDownloads(List<Downloader> runningDownloads) { + this.runningDownloads = runningDownloads; + notifyDataSetChanged(); } @Override public View getView(int position, View convertView, ViewGroup parent) { - DownloadItemViewHolder holder; + DownloadLogItemViewHolder holder; if (convertView == null) { - holder = new DownloadItemViewHolder(context, parent); + holder = new DownloadLogItemViewHolder(context, parent); + holder.itemView.setTag(holder); } else { - holder = (DownloadItemViewHolder) convertView.getTag(); + holder = (DownloadLogItemViewHolder) convertView.getTag(); } - DownloadStatus status = getItem(position); + Object item = getItem(position); + if (item instanceof DownloadStatus) { + bind(holder, (DownloadStatus) item, position); + } else if (item instanceof Downloader) { + bind(holder, (Downloader) item, position); + } + return holder.itemView; + } + + private void bind(DownloadLogItemViewHolder holder, DownloadStatus status, int position) { + String statusText = ""; if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - holder.type.setText(R.string.download_type_feed); + statusText += context.getString(R.string.download_type_feed); } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - holder.type.setText(R.string.download_type_media); + statusText += context.getString(R.string.download_type_media); } + statusText += " · "; + statusText += DateUtils.getRelativeTimeSpanString(status.getCompletionDate().getTime(), + System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0); + holder.status.setText(statusText); if (status.getTitle() != null) { holder.title.setText(status.getTitle()); } else { holder.title.setText(R.string.download_log_title_unknown); } - holder.date.setText(DateUtils.getRelativeTimeSpanString(status.getCompletionDate().getTime(), - System.currentTimeMillis(), 0, 0)); if (status.isSuccessful()) { holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_success_green)); @@ -77,13 +108,13 @@ public class DownloadLogAdapter extends BaseAdapter { holder.reason.setVisibility(View.VISIBLE); holder.tapForDetails.setVisibility(View.VISIBLE); - if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { + if (newerWasSuccessful(position - runningDownloads.size(), + status.getFeedfileType(), status.getFeedfileId())) { holder.secondaryActionButton.setVisibility(View.INVISIBLE); holder.secondaryActionButton.setOnClickListener(null); holder.secondaryActionButton.setTag(null); } else { - holder.secondaryActionIcon.setImageResource( - ThemeUtils.getDrawableFromAttr(context, R.attr.navigation_refresh)); + holder.secondaryActionIcon.setImageResource(R.drawable.ic_refresh); holder.secondaryActionButton.setVisibility(View.VISIBLE); if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { @@ -120,13 +151,51 @@ public class DownloadLogAdapter extends BaseAdapter { } } } + } - return holder.itemView; + private void bind(DownloadLogItemViewHolder holder, Downloader downloader, int position) { + DownloadRequest request = downloader.getDownloadRequest(); + holder.title.setText(request.getTitle()); + holder.secondaryActionIcon.setImageResource(R.drawable.ic_cancel); + holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); + holder.secondaryActionButton.setVisibility(View.VISIBLE); + holder.secondaryActionButton.setTag(downloader); + holder.secondaryActionButton.setOnClickListener(v -> + listFragment.onListItemClick(null, holder.itemView, position, 0)); + holder.reason.setVisibility(View.GONE); + holder.tapForDetails.setVisibility(View.GONE); + holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.colorPrimary)); + holder.icon.setText("{fa-arrow-circle-down}"); + holder.icon.setContentDescription(context.getString(R.string.status_downloading_label)); + + boolean percentageWasSet = false; + String status = ""; + if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + status += context.getString(R.string.download_type_feed); + } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + status += context.getString(R.string.download_type_media); + } + status += " · "; + if (request.getSoFar() <= 0) { + status += context.getString(R.string.download_pending); + } else { + status += Formatter.formatShortFileSize(context, request.getSoFar()); + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) { + status += " / " + Formatter.formatShortFileSize(context, request.getSize()); + holder.secondaryActionProgress.setPercentage( + 0.01f * Math.max(1, request.getProgressPercent()), request); + percentageWasSet = true; + } + } + if (!percentageWasSet) { + holder.secondaryActionProgress.setPercentage(0, request); + } + holder.status.setText(status); } - private boolean newerWasSuccessful(int position, int feedTypeId, long id) { - for (int i = 0; i < position; i++) { - DownloadStatus status = getItem(i); + private boolean newerWasSuccessful(int downloadStatusIndex, int feedTypeId, long id) { + for (int i = 0; i < downloadStatusIndex; i++) { + DownloadStatus status = downloadLog.get(i); if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id && status.isSuccessful()) { return true; } @@ -136,12 +205,17 @@ public class DownloadLogAdapter extends BaseAdapter { @Override public int getCount() { - return itemAccess.getCount(); + return downloadLog.size() + runningDownloads.size(); } @Override - public DownloadStatus getItem(int position) { - return itemAccess.getItem(position); + public Object getItem(int position) { + if (position < runningDownloads.size()) { + return runningDownloads.get(position); + } else if (position - runningDownloads.size() < downloadLog.size()) { + return downloadLog.get(position - runningDownloads.size()); + } + return null; } @Override @@ -149,10 +223,4 @@ public class DownloadLogAdapter extends BaseAdapter { return position; } - public interface ItemAccess { - int getCount(); - - DownloadStatus getItem(int position); - } - } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java deleted file mode 100644 index 9363edc9f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.text.format.Formatter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.service.download.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadStatus; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.common.CircularProgressBar; - -public class DownloadlistAdapter extends BaseAdapter { - - private final ItemAccess itemAccess; - private final Context context; - - public DownloadlistAdapter(Context context, ItemAccess itemAccess) { - super(); - this.context = context; - this.itemAccess = itemAccess; - } - - @Override - public int getCount() { - return itemAccess.getCount(); - } - - @Override - public Downloader getItem(int position) { - return itemAccess.getItem(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Holder holder; - Downloader downloader = getItem(position); - DownloadRequest request = downloader.getDownloadRequest(); - if (convertView == null) { - holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.downloadlist_item, parent, false); - holder.title = convertView.findViewById(R.id.txtvTitle); - holder.status = convertView.findViewById(R.id.txtvStatus); - holder.secondaryActionButton = convertView.findViewById(R.id.secondaryActionButton); - holder.secondaryActionIcon = convertView.findViewById(R.id.secondaryActionIcon); - holder.secondaryActionProgress = convertView.findViewById(R.id.secondaryActionProgress); - convertView.setTag(holder); - } else { - holder = (Holder) convertView.getTag(); - } - - holder.title.setText(request.getTitle()); - holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.navigation_cancel)); - holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); - holder.secondaryActionButton.setTag(downloader); - holder.secondaryActionButton.setOnClickListener(butSecondaryListener); - - boolean percentageWasSet = false; - String status = ""; - if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - status += context.getString(R.string.download_type_feed); - } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - status += context.getString(R.string.download_type_media); - } - status += " · "; - if (request.getSoFar() <= 0) { - status += context.getString(R.string.download_pending); - } else { - status += Formatter.formatShortFileSize(context, request.getSoFar()); - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) { - status += " / " + Formatter.formatShortFileSize(context, request.getSize()); - holder.secondaryActionProgress.setPercentage( - 0.01f * Math.max(1, request.getProgressPercent()), request); - percentageWasSet = true; - } - } - if (!percentageWasSet) { - holder.secondaryActionProgress.setPercentage(0, request); - } - holder.status.setText(status); - - return convertView; - } - - private final View.OnClickListener butSecondaryListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Downloader downloader = (Downloader) v.getTag(); - itemAccess.onSecondaryActionClick(downloader); - } - }; - - static class Holder { - TextView title; - TextView status; - View secondaryActionButton; - ImageView secondaryActionIcon; - CircularProgressBar secondaryActionProgress; - } - - public interface ItemAccess { - int getCount(); - - Downloader getItem(int position); - - void onSecondaryActionClick(Downloader downloader); - } - -} 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 4762622d1..774b97454 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java @@ -10,7 +10,7 @@ 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.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.fragment.ItemPagerFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 8cb0fd30a..62a97e849 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -12,14 +12,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.playback.RemoteMedia; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DateUtils; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; +import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; import java.util.List; @@ -59,7 +60,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { holder.title.setText(item.getTitle()); holder.pubDate.setText(DateUtils.formatAbbrev(getContext(), item.getPubDate())); if (item.getDescription() != null) { - String description = item.getDescription() + String description = HtmlToPlainText.getPlainText(item.getDescription()) .replaceAll("\n", " ") .replaceAll("\\s+", " ") .trim(); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java index dbb9ce0d0..92865e211 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java @@ -8,7 +8,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.ui.common.SquareImageView; 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 7c8943f36..ff0311ab6 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -1,28 +1,31 @@ package de.danoeh.antennapod.adapter; import android.app.Activity; -import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import android.util.TypedValue; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconTextView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.EpisodesFragment; @@ -42,10 +45,9 @@ import java.util.List; /** * BaseAdapter for the navigation drawer */ -public class NavListAdapter extends BaseAdapter +public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder> implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final int VIEW_TYPE_COUNT = 3; public static final int VIEW_TYPE_NAV = 0; public static final int VIEW_TYPE_SECTION_DIVIDER = 1; private static final int VIEW_TYPE_SUBSCRIPTION = 2; @@ -56,9 +58,8 @@ public class NavListAdapter extends BaseAdapter */ public static final String SUBSCRIPTION_LIST_TAG = "SubscriptionList"; - private static List<String> tags; - private static String[] titles; - + private final List<String> fragmentTags = new ArrayList<>(); + private final String[] titles; private final ItemAccess itemAccess; private final WeakReference<Activity> activity; public boolean showSubscriptionList = true; @@ -96,7 +97,8 @@ public class NavListAdapter extends BaseAdapter showSubscriptionList = false; } - tags = newTags; + fragmentTags.clear(); + fragmentTags.addAll(newTags); notifyDataSetChanged(); } @@ -105,47 +107,31 @@ public class NavListAdapter extends BaseAdapter return titles[index]; } - private Drawable getDrawable(String tag) { - Activity context = activity.get(); - if (context == null) { - return null; - } - int icon; + private @DrawableRes int getDrawable(String tag) { switch (tag) { case QueueFragment.TAG: - icon = R.attr.stat_playlist; - break; + return R.drawable.ic_playlist; case EpisodesFragment.TAG: - icon = R.attr.feed; - break; + return R.drawable.ic_feed; case DownloadsFragment.TAG: - icon = R.attr.av_download; - break; + return R.drawable.ic_download; case PlaybackHistoryFragment.TAG: - icon = R.attr.ic_history; - break; + return R.drawable.ic_history; case SubscriptionFragment.TAG: - icon = R.attr.ic_folder; - break; + return R.drawable.ic_folder; case AddFeedFragment.TAG: - icon = R.attr.content_new; - break; + return R.drawable.ic_add; default: - return null; + return 0; } - TypedArray ta = context.obtainStyledAttributes(new int[] { icon } ); - Drawable result = ta.getDrawable(0); - ta.recycle(); - return result; } - public List<String> getTags() { - return Collections.unmodifiableList(tags); + public List<String> getFragmentTags() { + return Collections.unmodifiableList(fragmentTags); } - @Override - public int getCount() { + public int getItemCount() { int baseCount = getSubscriptionOffset(); if (showSubscriptionList) { baseCount += itemAccess.getCount(); @@ -154,25 +140,20 @@ public class NavListAdapter extends BaseAdapter } @Override - public Object getItem(int position) { + public long getItemId(int position) { int viewType = getItemViewType(position); - if (viewType == VIEW_TYPE_NAV) { - return getLabel(tags.get(position)); - } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { - return ""; + if (viewType == VIEW_TYPE_SUBSCRIPTION) { + return itemAccess.getItem(position - getSubscriptionOffset()).id; + } else if (viewType == VIEW_TYPE_NAV) { + return -Math.abs((long) fragmentTags.get(position).hashCode()) - 1; // Folder IDs are >0 } else { - return itemAccess.getItem(position); + return 0; } } @Override - public long getItemId(int position) { - return position; - } - - @Override public int getItemViewType(int position) { - if (0 <= position && position < tags.size()) { + if (0 <= position && position < fragmentTags.size()) { return VIEW_TYPE_NAV; } else if (position < getSubscriptionOffset()) { return VIEW_TYPE_SECTION_DIVIDER; @@ -181,69 +162,67 @@ public class NavListAdapter extends BaseAdapter } } - @Override - public int getViewTypeCount() { - return VIEW_TYPE_COUNT; - } - public int getSubscriptionOffset() { - return tags.size() > 0 ? tags.size() + 1 : 0; + return fragmentTags.size() > 0 ? fragmentTags.size() + 1 : 0; } + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(activity.get()); + if (viewType == VIEW_TYPE_NAV) { + return new NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); + } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { + return new DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false)); + } else { + return new FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false)); + } + } @Override - public View getView(int position, View convertView, ViewGroup parent) { + public void onBindViewHolder(@NonNull Holder holder, int position) { int viewType = getItemViewType(position); - View v; + + holder.itemView.setOnCreateContextMenuListener(null); if (viewType == VIEW_TYPE_NAV) { - v = getNavView((String) getItem(position), position, convertView, parent); + bindNavView(getLabel(fragmentTags.get(position)), position, (NavHolder) holder); } else if (viewType == VIEW_TYPE_SECTION_DIVIDER) { - v = getSectionDividerView(convertView, parent); + bindSectionDivider((DividerHolder) holder); } else { - v = getFeedView(position, convertView, parent); + int itemPos = position - getSubscriptionOffset(); + NavDrawerData.DrawerItem item = itemAccess.getItem(itemPos); + bindListItem(item, (FeedHolder) holder); + if (item.type == NavDrawerData.DrawerItem.Type.FEED) { + bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder); + holder.itemView.setOnCreateContextMenuListener(itemAccess); + } else { + bindFolderView((NavDrawerData.FolderDrawerItem) item, (FeedHolder) holder); + } } - if (v != null && viewType != VIEW_TYPE_SECTION_DIVIDER) { + if (viewType != VIEW_TYPE_SECTION_DIVIDER) { TypedValue typedValue = new TypedValue(); - if (position == itemAccess.getSelectedItemIndex()) { - v.getContext().getTheme().resolveAttribute(R.attr.drawer_activated_color, typedValue, true); - v.setBackgroundResource(typedValue.resourceId); - } else { - v.getContext().getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true); - v.setBackgroundResource(typedValue.resourceId); - } + activity.get().getTheme().resolveAttribute(itemAccess.isSelected(position) + ? R.attr.drawer_activated_color : android.R.attr.windowBackground, typedValue, true); + holder.itemView.setBackgroundResource(typedValue.resourceId); + + holder.itemView.setOnClickListener(v -> itemAccess.onItemClick(position)); + holder.itemView.setOnLongClickListener(v -> itemAccess.onItemLongClick(position)); } - return v; } - private View getNavView(String title, int position, View convertView, ViewGroup parent) { + private void bindNavView(String title, int position, NavHolder holder) { Activity context = activity.get(); - if(context == null) { - return null; - } - NavHolder holder; - if (convertView == null) { - holder = new NavHolder(); - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - convertView = inflater.inflate(R.layout.nav_listitem, parent, false); - - holder.image = convertView.findViewById(R.id.imgvCover); - holder.title = convertView.findViewById(R.id.txtvTitle); - holder.count = convertView.findViewById(R.id.txtvCount); - convertView.setTag(holder); - } else { - holder = (NavHolder) convertView.getTag(); + if (context == null) { + return; } - holder.title.setText(title); // reset for re-use holder.count.setVisibility(View.GONE); holder.count.setOnClickListener(null); - String tag = tags.get(position); + String tag = fragmentTags.get(position); if (tag.equals(QueueFragment.TAG)) { int queueSize = itemAccess.getQueueSize(); if (queueSize > 0) { @@ -262,78 +241,69 @@ public class NavListAdapter extends BaseAdapter holder.count.setText(NumberFormat.getInstance().format(sum)); holder.count.setVisibility(View.VISIBLE); } - } else if(tag.equals(DownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) { + } else if (tag.equals(DownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) { int epCacheSize = UserPreferences.getEpisodeCacheSize(); // don't count episodes that can be reclaimed - int spaceUsed = itemAccess.getNumberOfDownloadedItems() - - itemAccess.getReclaimableItems(); + int spaceUsed = itemAccess.getNumberOfDownloadedItems() + - itemAccess.getReclaimableItems(); if (epCacheSize > 0 && spaceUsed >= epCacheSize) { holder.count.setText("{md-disc-full 150%}"); Iconify.addIcons(holder.count); holder.count.setVisibility(View.VISIBLE); holder.count.setOnClickListener(v -> - new AlertDialog.Builder(context) + new AlertDialog.Builder(context) .setTitle(R.string.episode_cache_full_title) .setMessage(R.string.episode_cache_full_message) - .setPositiveButton(android.R.string.ok, (dialog, which) -> {}) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.open_autodownload_settings, (dialog, which) -> { + Intent intent = new Intent(context, PreferenceActivity.class); + intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true); + context.startActivity(intent); + }) .show() ); } } - holder.image.setImageDrawable(getDrawable(tags.get(position))); - - return convertView; + holder.image.setImageResource(getDrawable(fragmentTags.get(position))); } - private View getSectionDividerView(View convertView, ViewGroup parent) { + private void bindSectionDivider(DividerHolder holder) { Activity context = activity.get(); - if(context == null) { - return null; + if (context == null) { + return; } - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - convertView = inflater.inflate(R.layout.nav_section_item, parent, false); - TextView feedsFilteredMsg = convertView.findViewById(R.id.nav_feeds_filtered_message); if (UserPreferences.getSubscriptionsFilter().isEnabled() && showSubscriptionList) { - convertView.setEnabled(true); - feedsFilteredMsg.setText("{md-info-outline} " + context.getString(R.string.subscriptions_are_filtered)); - Iconify.addIcons(feedsFilteredMsg); - feedsFilteredMsg.setVisibility(View.VISIBLE); + holder.itemView.setEnabled(true); + holder.feedsFilteredMsg.setText("{md-info-outline} " + + context.getString(R.string.subscriptions_are_filtered)); + Iconify.addIcons(holder.feedsFilteredMsg); + holder.feedsFilteredMsg.setVisibility(View.VISIBLE); } else { - convertView.setEnabled(false); - feedsFilteredMsg.setVisibility(View.GONE); + holder.itemView.setEnabled(false); + holder.feedsFilteredMsg.setVisibility(View.GONE); } + } - return convertView; + private void bindListItem(NavDrawerData.DrawerItem item, FeedHolder holder) { + if (item.getCounter() > 0) { + holder.count.setVisibility(View.VISIBLE); + holder.count.setText(NumberFormat.getInstance().format(item.getCounter())); + } else { + holder.count.setVisibility(View.GONE); + } + holder.title.setText(item.getTitle()); + int padding = (int) (activity.get().getResources().getDimension(R.dimen.thumbnail_length_navlist) / 2); + holder.itemView.setPadding(item.getLayer() * padding, 0, 0, 0); } - private View getFeedView(int position, View convertView, ViewGroup parent) { + private void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) { + Feed feed = drawerItem.feed; Activity context = activity.get(); - if(context == null) { - return null; - } - int feedPos = position - getSubscriptionOffset(); - Feed feed = itemAccess.getItem(feedPos); - - FeedHolder holder; - if (convertView == null) { - holder = new FeedHolder(); - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - convertView = inflater.inflate(R.layout.nav_listitem, parent, false); - - holder.image = convertView.findViewById(R.id.imgvCover); - holder.title = convertView.findViewById(R.id.txtvTitle); - holder.failure = convertView.findViewById(R.id.itxtvFailure); - holder.count = convertView.findViewById(R.id.txtvCount); - convertView.setTag(holder); - } else { - holder = (FeedHolder) convertView.getTag(); + if (context == null) { + return; } Glide.with(context) @@ -346,9 +316,7 @@ public class NavListAdapter extends BaseAdapter .dontAnimate()) .into(holder.image); - holder.title.setText(feed.getTitle()); - - if(feed.hasLastUpdateFailed()) { + if (feed.hasLastUpdateFailed()) { RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams(); p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure); holder.failure.setVisibility(View.VISIBLE); @@ -357,39 +325,87 @@ public class NavListAdapter extends BaseAdapter p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount); holder.failure.setVisibility(View.GONE); } - int counter = itemAccess.getFeedCounter(feed.getId()); - if(counter > 0) { - holder.count.setVisibility(View.VISIBLE); - holder.count.setText(NumberFormat.getInstance().format(counter)); - } else { + } + + private void bindFolderView(NavDrawerData.FolderDrawerItem folder, FeedHolder holder) { + Activity context = activity.get(); + if (context == null) { + return; + } + if (folder.isOpen) { holder.count.setVisibility(View.GONE); } - return convertView; + Glide.with(context).clear(holder.image); + holder.image.setImageResource(R.drawable.ic_folder); + holder.failure.setVisibility(View.GONE); + } + + static class Holder extends RecyclerView.ViewHolder { + public Holder(@NonNull View itemView) { + super(itemView); + } } - static class NavHolder { - ImageView image; - TextView title; - TextView count; + static class DividerHolder extends Holder { + final TextView feedsFilteredMsg; + + public DividerHolder(@NonNull View itemView) { + super(itemView); + feedsFilteredMsg = itemView.findViewById(R.id.nav_feeds_filtered_message); + } } - static class FeedHolder { - ImageView image; - TextView title; - IconTextView failure; - TextView count; + static class NavHolder extends Holder { + final ImageView image; + final TextView title; + final TextView count; + + public NavHolder(@NonNull View itemView) { + super(itemView); + image = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + count = itemView.findViewById(R.id.txtvCount); + } } - public interface ItemAccess { + static class FeedHolder extends Holder { + final ImageView image; + final TextView title; + final IconTextView failure; + final TextView count; + + public FeedHolder(@NonNull View itemView) { + super(itemView); + image = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + failure = itemView.findViewById(R.id.itxtvFailure); + count = itemView.findViewById(R.id.txtvCount); + } + } + + public interface ItemAccess extends View.OnCreateContextMenuListener { int getCount(); - Feed getItem(int position); - int getSelectedItemIndex(); + + NavDrawerData.DrawerItem getItem(int position); + + boolean isSelected(int position); + int getQueueSize(); + int getNumberOfNewItems(); + int getNumberOfDownloadedItems(); + int getReclaimableItems(); - int getFeedCounter(long feedId); + int getFeedCounterSum(); + + void onItemClick(int position); + + boolean onItemLongClick(int position); + + @Override + void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java index f7d6358de..0af6960d7 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java @@ -19,19 +19,17 @@ import java.util.Locale; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.fragment.SubscriptionFragment; import jp.shts.android.library.TriangleLabelView; /** * Adapter for subscriptions */ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnItemClickListener { - - /** placeholder object that indicates item should be added */ - public static final Object ADD_ITEM_OBJ = new Object(); - /** the position in the view that holds the add item; 0 is the first, -1 is the last position */ private static final String TAG = "SubscriptionsAdapter"; @@ -60,7 +58,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI @Override public long getItemId(int position) { - return itemAccess.getItem(position).getId(); + return ((NavDrawerData.DrawerItem) getItem(position)).id; } @Override @@ -83,11 +81,13 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI holder = (Holder) convertView.getTag(); } - final Feed feed = (Feed) getItem(position); - if (feed == null) return null; + final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position); + if (drawerItem == null) { + return null; + } - holder.feedTitle.setText(feed.getTitle()); - holder.imageView.setContentDescription(feed.getTitle()); + holder.feedTitle.setText(drawerItem.getTitle()); + holder.imageView.setContentDescription(drawerItem.getTitle()); holder.feedTitle.setVisibility(View.VISIBLE); // Fix TriangleLabelView corner for RTL @@ -96,30 +96,46 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI holder.count.setCorner(TriangleLabelView.Corner.TOP_LEFT); } - int count = itemAccess.getFeedCounter(feed.getId()); - if(count > 0) { - holder.count.setPrimaryText( - NumberFormat.getInstance().format(itemAccess.getFeedCounter(feed.getId()))); + if (drawerItem.getCounter() > 0) { + holder.count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter())); holder.count.setVisibility(View.VISIBLE); } else { holder.count.setVisibility(View.GONE); } - boolean textAndImageCombined = feed.isLocalFeed() - && LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl()); - new CoverLoader(mainActivityRef.get()) - .withUri(feed.getImageUrl()) - .withPlaceholderView(holder.feedTitle, textAndImageCombined) - .withCoverView(holder.imageView) - .load(); - + if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { + Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; + boolean textAndImageCombined = feed.isLocalFeed() + && LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl()); + new CoverLoader(mainActivityRef.get()) + .withUri(feed.getImageUrl()) + .withPlaceholderView(holder.feedTitle, textAndImageCombined) + .withCoverView(holder.imageView) + .load(); + } else { + new CoverLoader(mainActivityRef.get()) + .withResource(R.drawable.ic_folder) + .withPlaceholderView(holder.feedTitle, true) + .withCoverView(holder.imageView) + .load(); + } return convertView; } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Fragment fragment = FeedItemlistFragment.newInstance(getItemId(position)); - mainActivityRef.get().loadChildFragment(fragment); + final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position); + if (drawerItem == null) { + return; + } + if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { + Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed; + Fragment fragment = FeedItemlistFragment.newInstance(feed.getId()); + mainActivityRef.get().loadChildFragment(fragment); + } else if (drawerItem.type == NavDrawerData.DrawerItem.Type.FOLDER) { + Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle()); + mainActivityRef.get().loadChildFragment(fragment); + } } static class Holder { @@ -130,7 +146,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI public interface ItemAccess { int getCount(); - Feed getItem(int position); - int getFeedCounter(long feedId); + + NavDrawerData.DrawerItem getItem(int position); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java index a8001eeb1..b362a5a1d 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java @@ -1,11 +1,11 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; class AddToQueueActionButton extends ItemActionButton { @@ -20,9 +20,9 @@ class AddToQueueActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.content_new; + return R.drawable.ic_add; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java index a31d2fdc0..afa86c9d7 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java @@ -1,13 +1,13 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import android.widget.Toast; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -25,9 +25,9 @@ public class CancelDownloadActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.navigation_cancel; + return R.drawable.ic_cancel; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java index 45cce23b8..096d060c1 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java @@ -2,11 +2,11 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; import android.view.View; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.storage.DBWriter; public class DeleteActionButton extends ItemActionButton { @@ -22,9 +22,9 @@ public class DeleteActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.ic_delete; + return R.drawable.ic_delete; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java index 0f7c2bdd0..c3e979dd8 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java @@ -4,14 +4,14 @@ import android.content.Context; import android.view.View; import android.widget.Toast; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UsageStatistics; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; @@ -33,9 +33,9 @@ public class DownloadActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.av_download; + return R.drawable.ic_download; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java index 5d95d3775..12150293f 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java @@ -1,17 +1,17 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import android.content.res.TypedArray; import android.widget.ImageView; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import android.view.View; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.FeedItemUtil; public abstract class ItemActionButton { FeedItem item; @@ -23,7 +23,7 @@ public abstract class ItemActionButton { @StringRes public abstract int getLabel(); - @AttrRes + @DrawableRes public abstract int getDrawable(); public abstract void onClick(Context context); @@ -40,7 +40,7 @@ public abstract class ItemActionButton { } final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); - if (media.isCurrentlyPlaying()) { + if (FeedItemUtil.isCurrentlyPlaying(media)) { return new PauseActionButton(item); } else if (item.getFeed().isLocalFeed()) { return new PlayLocalActionButton(item); @@ -62,9 +62,6 @@ public abstract class ItemActionButton { button.setVisibility(getVisibility()); button.setContentDescription(context.getString(getLabel())); button.setOnClickListener((view) -> onClick(context)); - - TypedArray drawables = context.obtainStyledAttributes(new int[]{getDrawable()}); - icon.setImageDrawable(drawables.getDrawable(0)); - drawables.recycle(); + icon.setImageResource(getDrawable()); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java index 14fa94f7a..8dc4ffe33 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java @@ -1,12 +1,12 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import android.view.View; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBWriter; public class MarkAsPlayedActionButton extends ItemActionButton { @@ -22,9 +22,9 @@ public class MarkAsPlayedActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.navigation_accept; + return R.drawable.ic_check; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java index 49b785056..66ce973e2 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java @@ -5,7 +5,7 @@ import android.content.Context; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java index 4ac03c50e..de4dae6a0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PauseActionButton.java @@ -1,11 +1,12 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import static de.danoeh.antennapod.core.service.playback.PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE; @@ -23,9 +24,9 @@ public class PauseActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.av_pause; + return R.drawable.ic_pause; } @Override @@ -35,7 +36,7 @@ public class PauseActionButton extends ItemActionButton { return; } - if (media.isCurrentlyPlaying()) { + if (FeedItemUtil.isCurrentlyPlaying(media)) { IntentUtils.sendLocalBroadcast(context, ACTION_PAUSE_PLAY_CURRENT_EPISODE); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java index 512f1a512..974b12bab 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java @@ -1,12 +1,12 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; @@ -24,9 +24,9 @@ public class PlayActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.av_play; + return R.drawable.ic_play_24dp; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java index 78ea3b93f..ab2122b12 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java @@ -1,12 +1,12 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; @@ -23,9 +23,9 @@ public class PlayLocalActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.av_play; + return R.drawable.ic_play_24dp; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java index 8a892a621..94c446f50 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java @@ -2,13 +2,13 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.preferences.UsageStatistics; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -28,9 +28,9 @@ public class StreamActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.action_stream; + return R.drawable.ic_stream; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java index e45280eed..03ccce2fe 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/VisitWebsiteActionButton.java @@ -2,10 +2,10 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; import android.view.View; -import androidx.annotation.AttrRes; +import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.util.IntentUtils; public class VisitWebsiteActionButton extends ItemActionButton { @@ -21,9 +21,9 @@ public class VisitWebsiteActionButton extends ItemActionButton { } @Override - @AttrRes + @DrawableRes public int getDrawable() { - return R.attr.location_web_site; + return R.drawable.ic_web; } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java index 25fc0a05c..a125515cc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -13,7 +13,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; import org.apache.commons.lang3.StringUtils; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java index 698e43145..b0441688d 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java @@ -8,7 +8,7 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java index 906d50c61..73e19c746 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java @@ -5,7 +5,6 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -37,9 +36,6 @@ public class DocumentFileExportWorker { OutputStreamWriter writer = null; try { Uri uri = output.getUri(); - if (uri == null) { - throw new FileNotFoundException("Export file not found."); - } outputStream = context.getContentResolver().openOutputStream(uri); if (outputStream == null) { throw new IOException(); diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java index 0930b59eb..af9f041af 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java @@ -41,8 +41,8 @@ public class ExportWorker { public Observable<File> exportObservable() { if (output.exists()) { - Log.w(TAG, "Overwriting previously exported file."); - output.delete(); + boolean success = output.delete(); + Log.w(TAG, "Overwriting previously exported file: " + success); } return Observable.create(subscriber -> { OutputStreamWriter writer = null; diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java index ea5128102..a80e3d59b 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java @@ -9,7 +9,7 @@ import java.util.Arrays; import de.danoeh.antennapod.activity.OpmlImportHolder; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.export.opml.OpmlElement; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; diff --git a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java index f782308d1..938bb5931 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/DownloadServiceCallbacksImpl.java @@ -21,7 +21,7 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks { Intent intent = new Intent(context, MainActivity.class); intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, DownloadsFragment.TAG); Bundle args = new Bundle(); - args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); + args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java index eb33da037..ee19a0339 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java @@ -8,7 +8,7 @@ import android.widget.RadioButton; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedFilter; +import de.danoeh.antennapod.model.feed.FeedFilter; /** * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index e1e8f1c2e..508ce74f4 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -21,14 +21,13 @@ import com.leinardi.android.speeddial.SpeedDialView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.SortOrder; -import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.model.feed.SortOrder; import java.util.ArrayList; import java.util.Arrays; @@ -215,10 +214,10 @@ public class EpisodesApplyActionFragment extends Fragment implements Toolbar.OnM public void refreshToolbarState() { MenuItem selectAllItem = toolbar.getMenu().findItem(R.id.select_toggle); if (checkedIds.size() == episodes.size()) { - selectAllItem.setIcon(ThemeUtils.getDrawableFromAttr(getContext(), R.attr.ic_select_none)); + selectAllItem.setIcon(R.drawable.ic_select_none); selectAllItem.setTitle(R.string.deselect_all_label); } else { - selectAllItem.setIcon(ThemeUtils.getDrawableFromAttr(getContext(), R.attr.ic_select_all)); + selectAllItem.setIcon(R.drawable.ic_select_all); selectAllItem.setTitle(R.string.select_all_label); } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java new file mode 100644 index 000000000..7e42302d1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedRefreshIntervalDialog.java @@ -0,0 +1,106 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import androidx.appcompat.app.AlertDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.databinding.FeedRefreshDialogBinding; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.concurrent.TimeUnit; + +public class FeedRefreshIntervalDialog { + private static final int[] INTERVAL_VALUES_HOURS = {1, 2, 4, 8, 12, 24, 72}; + private final Context context; + private FeedRefreshDialogBinding viewBinding; + + public FeedRefreshIntervalDialog(Context context) { + this.context = context; + } + + public void show() { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.feed_refresh_title); + builder.setMessage(R.string.feed_refresh_sum); + viewBinding = FeedRefreshDialogBinding.inflate(LayoutInflater.from(context)); + builder.setView(viewBinding.getRoot()); + + ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(context, + android.R.layout.simple_spinner_item, buildSpinnerEntries()); + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + viewBinding.spinner.setAdapter(spinnerArrayAdapter); + viewBinding.timePicker.setIs24HourView(DateFormat.is24HourFormat(context)); + viewBinding.spinner.setSelection(ArrayUtils.indexOf(INTERVAL_VALUES_HOURS, 24)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + viewBinding.timePicker.setHour(8); + viewBinding.timePicker.setMinute(0); + } else { + viewBinding.timePicker.setCurrentHour(8); + viewBinding.timePicker.setCurrentMinute(0); + } + + long currInterval = UserPreferences.getUpdateInterval(); + int[] updateTime = UserPreferences.getUpdateTimeOfDay(); + if (currInterval > 0) { + viewBinding.spinner.setSelection(ArrayUtils.indexOf(INTERVAL_VALUES_HOURS, + (int) TimeUnit.MILLISECONDS.toHours(currInterval))); + viewBinding.intervalRadioButton.setChecked(true); + } else if (updateTime.length == 2 && updateTime[0] >= 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + viewBinding.timePicker.setHour(updateTime[0]); + viewBinding.timePicker.setMinute(updateTime[1]); + } else { + viewBinding.timePicker.setCurrentHour(updateTime[0]); + viewBinding.timePicker.setCurrentMinute(updateTime[1]); + } + viewBinding.timeRadioButton.setChecked(true); + } else { + viewBinding.disableRadioButton.setChecked(true); + } + updateVisibility(); + + viewBinding.radioGroup.setOnCheckedChangeListener((radioGroup, i) -> updateVisibility()); + + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + if (viewBinding.intervalRadioButton.isChecked()) { + UserPreferences.setUpdateInterval(INTERVAL_VALUES_HOURS[viewBinding.spinner.getSelectedItemPosition()]); + } else if (viewBinding.timeRadioButton.isChecked()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getHour(), + viewBinding.timePicker.getMinute()); + } else { + UserPreferences.setUpdateTimeOfDay(viewBinding.timePicker.getCurrentHour(), + viewBinding.timePicker.getCurrentMinute()); + } + } else if (viewBinding.disableRadioButton.isChecked()) { + UserPreferences.disableAutoUpdate(context); + } else { + throw new IllegalStateException("Unexpected error."); + } + }); + + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + + private String[] buildSpinnerEntries() { + final Resources res = context.getResources(); + String[] entries = new String[INTERVAL_VALUES_HOURS.length]; + for (int i = 0; i < INTERVAL_VALUES_HOURS.length; i++) { + int hours = INTERVAL_VALUES_HOURS[i]; + entries[i] = res.getQuantityString(R.plurals.feed_refresh_every_x_hours, hours, hours); + } + return entries; + } + + private void updateVisibility() { + viewBinding.spinner.setVisibility(viewBinding.intervalRadioButton.isChecked() ? View.VISIBLE : View.GONE); + viewBinding.timePicker.setVisibility(viewBinding.timeRadioButton.isChecked() ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java index 779248e2f..a38ca6b71 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java @@ -14,7 +14,7 @@ import java.util.HashSet; import java.util.Set; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedItemFilterGroup; import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java index 7e0e59776..58f070046 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java @@ -7,7 +7,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.model.feed.SortOrder; public abstract class IntraFeedSortDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java index d0fb91692..13258b4ec 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java @@ -30,8 +30,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.ProxyConfig; -import io.reactivex.Single; -import io.reactivex.SingleOnSubscribe; +import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @@ -41,9 +40,6 @@ import okhttp3.Request; import okhttp3.Response; public class ProxyDialog { - - private static final String TAG = "ProxyDialog"; - private final Context context; private AlertDialog dialog; @@ -116,32 +112,32 @@ public class ProxyDialog { types.add(Proxy.Type.SOCKS.name()); } ArrayAdapter<String> adapter = new ArrayAdapter<>(context, - android.R.layout.simple_spinner_item, types); + android.R.layout.simple_spinner_item, types); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spType.setAdapter(adapter); ProxyConfig proxyConfig = UserPreferences.getProxyConfig(); spType.setSelection(adapter.getPosition(proxyConfig.type.name())); etHost = content.findViewById(R.id.etHost); - if(!TextUtils.isEmpty(proxyConfig.host)) { + if (!TextUtils.isEmpty(proxyConfig.host)) { etHost.setText(proxyConfig.host); } etHost.addTextChangedListener(requireTestOnChange); etPort = content.findViewById(R.id.etPort); - if(proxyConfig.port > 0) { + if (proxyConfig.port > 0) { etPort.setText(String.valueOf(proxyConfig.port)); } etPort.addTextChangedListener(requireTestOnChange); etUsername = content.findViewById(R.id.etUsername); - if(!TextUtils.isEmpty(proxyConfig.username)) { + if (!TextUtils.isEmpty(proxyConfig.username)) { etUsername.setText(proxyConfig.username); } etUsername.addTextChangedListener(requireTestOnChange); etPassword = content.findViewById(R.id.etPassword); - if(!TextUtils.isEmpty(proxyConfig.password)) { - etPassword.setText(proxyConfig.username); + if (!TextUtils.isEmpty(proxyConfig.password)) { + etPassword.setText(proxyConfig.password); } etPassword.addTextChangedListener(requireTestOnChange); - if(proxyConfig.type == Proxy.Type.DIRECT) { + if (proxyConfig.type == Proxy.Type.DIRECT) { enableSettings(false); setTestRequired(false); } @@ -184,8 +180,8 @@ public class ProxyDialog { private boolean checkValidity() { boolean valid = true; - if(spType.getSelectedItemPosition() > 0) { - valid &= checkHost(); + if (spType.getSelectedItemPosition() > 0) { + valid = checkHost(); } valid &= checkPort(); return valid; @@ -193,11 +189,11 @@ public class ProxyDialog { private boolean checkHost() { String host = etHost.getText().toString(); - if(host.length() == 0) { + if (host.length() == 0) { etHost.setError(context.getString(R.string.proxy_host_empty_error)); return false; } - if(!"localhost".equals(host) && !Patterns.DOMAIN_NAME.matcher(host).matches()) { + if (!"localhost".equals(host) && !Patterns.DOMAIN_NAME.matcher(host).matches()) { etHost.setError(context.getString(R.string.proxy_host_invalid_error)); return false; } @@ -206,7 +202,7 @@ public class ProxyDialog { private boolean checkPort() { int port = getPort(); - if(port < 0 && port > 65535) { + if (port < 0 || port > 65535) { etPort.setError(context.getString(R.string.proxy_port_invalid_error)); return false; } @@ -215,10 +211,10 @@ public class ProxyDialog { private int getPort() { String port = etPort.getText().toString(); - if(port.length() > 0) { + if (port.length() > 0) { try { return Integer.parseInt(port); - } catch(NumberFormatException e) { + } catch (NumberFormatException e) { // ignore } } @@ -226,7 +222,7 @@ public class ProxyDialog { } private void setTestRequired(boolean required) { - if(required) { + if (required) { testSuccessful = false; dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.proxy_test_label); } else { @@ -240,7 +236,7 @@ public class ProxyDialog { if (disposable != null) { disposable.dispose(); } - if(!checkValidity()) { + if (!checkValidity()) { setTestRequired(true); return; } @@ -251,7 +247,7 @@ public class ProxyDialog { txtvMessage.setTextColor(textColorPrimary); txtvMessage.setText("{fa-circle-o-notch spin} " + checking); txtvMessage.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe<Response>) emitter -> { + disposable = Completable.create(emitter -> { String type = (String) spType.getSelectedItem(); String host = etHost.getText().toString(); String port = etPort.getText().toString(); @@ -263,59 +259,44 @@ public class ProxyDialog { } SocketAddress address = InetSocketAddress.createUnresolved(host, portValue); Proxy.Type proxyType = Proxy.Type.valueOf(type.toUpperCase(Locale.US)); - Proxy proxy = new Proxy(proxyType, address); OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder() .connectTimeout(10, TimeUnit.SECONDS) - .proxy(proxy); - builder.interceptors().clear(); - OkHttpClient client = builder.build(); + .proxy(new Proxy(proxyType, address)); if (!TextUtils.isEmpty(username)) { - String credentials = Credentials.basic(username, password); - client.interceptors().add(chain -> { - Request request = chain.request().newBuilder() - .header("Proxy-Authorization", credentials).build(); - return chain.proceed(request); + builder.proxyAuthenticator((route, response) -> { + String credentials = Credentials.basic(username, password); + return response.request().newBuilder() + .header("Proxy-Authorization", credentials) + .build(); }); } - Request request = new Request.Builder() - .url("http://www.google.com") - .head() - .build(); - try { - Response response = client.newCall(request).execute(); - emitter.onSuccess(response); - } catch(IOException e) { + OkHttpClient client = builder.build(); + Request request = new Request.Builder().url("https://www.example.com").head().build(); + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + emitter.onComplete(); + } else { + emitter.onError(new IOException(response.message())); + } + } catch (IOException e) { emitter.onError(e); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - response -> { - int colorId; - String icon; - String result; - if(response.isSuccessful()) { - colorId = R.color.download_success_green; - icon = "{fa-check}"; - result = context.getString(R.string.proxy_test_successful); - } else { - colorId = R.color.download_failed_red; - icon = "{fa-close}"; - result = context.getString(R.string.proxy_test_failed); - } - int color = ContextCompat.getColor(context, colorId); - txtvMessage.setTextColor(color); - String message = String.format("%s %s: %s", icon, result, response.message()); + () -> { + txtvMessage.setTextColor(ContextCompat.getColor(context, R.color.download_success_green)); + String message = String.format("%s %s", "{fa-check}", + context.getString(R.string.proxy_test_successful)); txtvMessage.setText(message); - setTestRequired(!response.isSuccessful()); + setTestRequired(false); }, error -> { - String icon = "{fa-close}"; - String result = context.getString(R.string.proxy_test_failed); - int color = ContextCompat.getColor(context, R.color.download_failed_red); - txtvMessage.setTextColor(color); - String message = String.format("%s %s: %s", icon, result, error.getMessage()); + error.printStackTrace(); + txtvMessage.setTextColor(ContextCompat.getColor(context, R.color.download_failed_red)); + String message = String.format("%s %s: %s", "{fa-close}", + context.getString(R.string.proxy_test_failed), error.getMessage()); txtvMessage.setText(message); setTestRequired(true); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java index 156c1dba8..f42f0870d 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java @@ -6,7 +6,7 @@ import android.content.DialogInterface; import android.util.Log; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DBWriter; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java index a4e49ff96..42a854cd8 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RenameFeedDialog.java @@ -7,7 +7,7 @@ import java.lang.ref.WeakReference; import android.view.View; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.databinding.EditTextDialogBinding; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java index 614cc1e71..37faea46f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/ShareDialog.java @@ -14,7 +14,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.util.ShareUtils; public class ShareDialog extends DialogFragment { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java index 8e5ceece2..22f62d410 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/StreamingConfirmationDialog.java @@ -4,7 +4,7 @@ import android.content.Context; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; public class StreamingConfirmationDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java new file mode 100644 index 000000000..de9f4d504 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.chip.Chip; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.databinding.EditTagsDialogBinding; +import de.danoeh.antennapod.view.ItemOffsetDecoration; + +import java.util.ArrayList; +import java.util.List; + +public class TagSettingsDialog extends DialogFragment { + public static final String TAG = "TagSettingsDialog"; + private static final String ARG_FEED_PREFERENCES = "feed_preferences"; + private List<String> displayedTags; + private EditTagsDialogBinding viewBinding; + private TagSelectionAdapter adapter; + + public static TagSettingsDialog newInstance(FeedPreferences preferences) { + TagSettingsDialog fragment = new TagSettingsDialog(); + Bundle args = new Bundle(); + args.putSerializable(ARG_FEED_PREFERENCES, preferences); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + FeedPreferences preferences = (FeedPreferences) getArguments().getSerializable(ARG_FEED_PREFERENCES); + displayedTags = new ArrayList<>(preferences.getTags()); + displayedTags.remove(FeedPreferences.TAG_ROOT); + + viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater()); + viewBinding.tagsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2)); + viewBinding.tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4)); + adapter = new TagSelectionAdapter(); + adapter.setHasStableIds(true); + viewBinding.tagsRecycler.setAdapter(adapter); + viewBinding.rootFolderCheckbox.setChecked(preferences.getTags().contains(FeedPreferences.TAG_ROOT)); + + viewBinding.newTagButton.setOnClickListener(v -> + addTag(viewBinding.newTagEditText.getText().toString().trim())); + + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + dialog.setView(viewBinding.getRoot()); + dialog.setTitle(R.string.feed_folders_label); + dialog.setPositiveButton(android.R.string.ok, (d, input) -> { + addTag(viewBinding.newTagEditText.getText().toString().trim()); + preferences.getTags().clear(); + preferences.getTags().addAll(displayedTags); + if (viewBinding.rootFolderCheckbox.isChecked()) { + preferences.getTags().add(FeedPreferences.TAG_ROOT); + } + DBWriter.setFeedPreferences(preferences); + }); + dialog.setNegativeButton(R.string.cancel_label, null); + return dialog.create(); + } + + private void addTag(String name) { + if (TextUtils.isEmpty(name) || displayedTags.contains(name)) { + return; + } + displayedTags.add(name); + viewBinding.newTagEditText.setText(""); + adapter.notifyDataSetChanged(); + } + + public class TagSelectionAdapter extends RecyclerView.Adapter<TagSelectionAdapter.ViewHolder> { + + @Override + @NonNull + public TagSelectionAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Chip chip = new Chip(getContext()); + chip.setCloseIconVisible(true); + chip.setCloseIconResource(R.drawable.ic_delete); + return new TagSelectionAdapter.ViewHolder(chip); + } + + @Override + public void onBindViewHolder(@NonNull TagSelectionAdapter.ViewHolder holder, int position) { + holder.chip.setText(displayedTags.get(position)); + holder.chip.setOnCloseIconClickListener(v -> { + displayedTags.remove(position); + notifyDataSetChanged(); + }); + } + + @Override + public int getItemCount() { + return displayedTags.size(); + } + + @Override + public long getItemId(int position) { + return displayedTags.get(position).hashCode(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + Chip chip; + + ViewHolder(Chip itemView) { + super(itemView); + chip = itemView; + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java index 65e7c4424..c6927c69f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.dialog; import android.app.Dialog; -import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; @@ -41,16 +40,6 @@ public class VariableSpeedDialog extends DialogFragment { selectedSpeeds = new ArrayList<>(UserPreferences.getPlaybackSpeedArray()); } - public static void showGetPluginDialog(final Context context) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.no_playback_plugin_title); - builder.setMessage(R.string.no_playback_plugin_or_sonic_msg); - builder.setPositiveButton(R.string.enable_sonic, (dialog, which) -> - UserPreferences.enableSonic()); - builder.setNeutralButton(R.string.close_label, null); - builder.show(); - } - @Override public void onStart() { super.onStart(); @@ -98,7 +87,7 @@ public class VariableSpeedDialog extends DialogFragment { addCurrentSpeedChip = root.findViewById(R.id.add_current_speed_chip); addCurrentSpeedChip.setCloseIconVisible(true); - addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add_black); + addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add); addCurrentSpeedChip.setOnCloseIconClickListener(v -> addCurrentSpeed()); addCurrentSpeedChip.setOnClickListener(v -> addCurrentSpeed()); @@ -126,7 +115,7 @@ public class VariableSpeedDialog extends DialogFragment { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { Chip chip = new Chip(getContext()); chip.setCloseIconVisible(true); - chip.setCloseIconResource(R.drawable.ic_delete_black); + chip.setCloseIconResource(R.drawable.ic_delete); return new ViewHolder(chip); } diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java index 6de2186e0..f97c1c7ab 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java @@ -2,9 +2,9 @@ package de.danoeh.antennapod.discovery; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -18,7 +18,8 @@ public class GpodnetPodcastSearcher implements PodcastSearcher { return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { try { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHosturl()); + GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), + GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0); List<PodcastSearchResult> results = new ArrayList<>(); for (GpodnetPodcast podcast : gpodnetPodcasts) { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java index e1034f89b..e4135fcaa 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.discovery; import android.content.Context; -import android.content.SharedPreferences; import android.util.Log; import de.danoeh.antennapod.R; @@ -24,8 +23,6 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; -import static android.content.Context.MODE_PRIVATE; - public class ItunesTopListLoader { private static final String TAG = "ITunesTopListLoader"; private final Context context; @@ -38,13 +35,6 @@ public class ItunesTopListLoader { this.context = context; } - public Single<List<PodcastSearchResult>> loadToplist() { - String defaultCountry = Locale.getDefault().getCountry(); - SharedPreferences prefs = context.getSharedPreferences(PREFS, MODE_PRIVATE); - String countryCode = prefs.getString(PREF_KEY_COUNTRY_CODE, COUNTRY_CODE_UNSET); - return this.loadToplist(countryCode, 25); - } - public Single<List<PodcastSearchResult>> loadToplist(String country, int limit) { return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> { OkHttpClient client = AntennapodHttpClient.getHttpClient(); diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java index bba438d1d..767845cb4 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.discovery; import androidx.annotation.Nullable; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; import de.mfietz.fyydlin.SearchHit; import org.json.JSONArray; import org.json.JSONException; diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java index 16c5548be..dfea627df 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java @@ -12,7 +12,7 @@ public class PodcastSearcherRegistry { private PodcastSearcherRegistry() { } - public static List<SearcherInfo> getSearchProviders() { + public static synchronized List<SearcherInfo> getSearchProviders() { if (searchProviders == null) { searchProviders = new ArrayList<>(); searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f)); diff --git a/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java b/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java index dc62863f9..23c8ffdd5 100644 --- a/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java +++ b/app/src/main/java/de/danoeh/antennapod/error/CrashReportWriter.java @@ -7,7 +7,6 @@ import de.danoeh.antennapod.BuildConfig; import org.apache.commons.io.IOUtils; import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; @@ -40,7 +39,7 @@ public class CrashReportWriter implements Thread.UncaughtExceptionHandler { File path = getFile(); PrintWriter out = null; try { - out = new PrintWriter(new FileWriter(path)); + out = new PrintWriter(path, "UTF-8"); out.println("## Crash info"); out.println("Time: " + new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.getDefault()).format(new Date())); out.println("AntennaPod version: " + BuildConfig.VERSION_NAME); 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 08e23fc7f..64e7f161e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -25,10 +25,10 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.activity.OpmlImportActivity; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; -import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.databinding.AddfeedBinding; import de.danoeh.antennapod.databinding.EditTextDialogBinding; import de.danoeh.antennapod.discovery.CombinedSearcher; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index 612959c04..f65c6fdc6 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -10,8 +10,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.joanzapata.iconify.Iconify; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.dialog.FilterDialog; import org.apache.commons.lang3.StringUtils; @@ -27,7 +27,7 @@ public class AllEpisodesFragment extends EpisodesListFragment { private static final String PREF_NAME = "PrefAllEpisodesFragment"; private static final String PREF_FILTER = "filter"; - private static FeedItemFilter feedItemFilter = new FeedItemFilter(""); + private FeedItemFilter feedItemFilter = new FeedItemFilter(""); @Override public void onCreate(@Nullable Bundle savedInstanceState) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index 1580ac930..64cbaa023 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -11,6 +11,7 @@ import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -20,26 +21,37 @@ import androidx.fragment.app.Fragment; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; + import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.event.ServiceEvent; +import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.dialog.PlaybackControlsDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog; @@ -47,28 +59,22 @@ import de.danoeh.antennapod.dialog.SleepTimerDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; +import de.danoeh.antennapod.view.ChapterSeekBar; +import de.danoeh.antennapod.view.PlayButton; import io.reactivex.Maybe; 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.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.List; /** * Shows the audio player. */ public class AudioPlayerFragment extends Fragment implements - SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { + ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { public static final String TAG = "AudioPlayerFragment"; - private static final int POS_COVER = 0; - private static final int POS_SHOWNOTES = 1; - private static final int POS_CHAPTERS = 2; - private static final int NUM_CONTENT_FRAGMENTS = 3; + public static final int POS_COVER = 0; + public static final int POS_DESCRIPTION = 1; + private static final int NUM_CONTENT_FRAGMENTS = 2; private static final float EPSILON = 0.001f; PlaybackSpeedIndicatorView butPlaybackSpeed; @@ -76,10 +82,10 @@ public class AudioPlayerFragment extends Fragment implements private ViewPager2 pager; private TextView txtvPosition; private TextView txtvLength; - private SeekBar sbPosition; + private ChapterSeekBar sbPosition; private ImageButton butRev; private TextView txtvRev; - private ImageButton butPlay; + private PlayButton butPlay; private ImageButton butFF; private TextView txtvFF; private ImageButton butSkip; @@ -91,8 +97,9 @@ public class AudioPlayerFragment extends Fragment implements private PlaybackController controller; private Disposable disposable; private boolean showTimeLeft; - private boolean hasChapters = false; - private TabLayoutMediator tabLayoutMediator; + private boolean seekedToChapterStart = false; + private int currentChapterIndex = -1; + private int duration; @Override public View onCreateView(@NonNull LayoutInflater inflater, @@ -148,34 +155,27 @@ public class AudioPlayerFragment extends Fragment implements } }); - TabLayout tabLayout = root.findViewById(R.id.sliding_tabs); - tabLayoutMediator = new TabLayoutMediator(tabLayout, pager, (tab, position) -> { - tab.view.setAlpha(1.0f); - switch (position) { - case POS_COVER: - tab.setText(R.string.cover_label); - break; - case POS_SHOWNOTES: - tab.setText(R.string.shownotes_label); - break; - case POS_CHAPTERS: - tab.setText(R.string.chapters_label); - if (!hasChapters) { - tab.view.setAlpha(0.5f); - } - break; - default: - break; - } - }); - tabLayoutMediator.attach(); return root; } - public void setHasChapters(boolean hasChapters) { - this.hasChapters = hasChapters; - tabLayoutMediator.detach(); - tabLayoutMediator.attach(); + private void setChapterDividers(Playable media) { + + if (media == null) { + return; + } + + float[] dividerPos = null; + + if (media.getChapters() != null) { + List<Chapter> chapters = media.getChapters(); + dividerPos = new float[chapters.size()]; + + for (int i = 0; i < chapters.size(); i++) { + dividerPos[i] = chapters.get(i).getStart() / (float) duration; + } + } + + sbPosition.setDividerPos(dividerPos); } public View getExternalPlayerHolder() { @@ -224,6 +224,13 @@ public class AudioPlayerFragment extends Fragment implements controller.getDuration())); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(ServiceEvent event) { + if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) { + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } + } + private void setupLengthTextView() { showTimeLeft = UserPreferences.shouldShowRemainingTime(); txtvLength.setOnClickListener(v -> { @@ -242,10 +249,6 @@ public class AudioPlayerFragment extends Fragment implements if (controller == null) { return; } - if (!controller.canSetPlaybackSpeed()) { - VariableSpeedDialog.showGetPluginDialog(getContext()); - return; - } List<Float> availableSpeeds = UserPreferences.getPlaybackSpeedArray(); float currentSpeed = controller.getCurrentPlaybackSpeedMultiplier(); @@ -279,14 +282,10 @@ public class AudioPlayerFragment extends Fragment implements if (butPlaybackSpeed == null || controller == null) { return; } - float speed = 1.0f; - if (controller.canSetPlaybackSpeed()) { - speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(media); - } + float speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(media); String speedStr = new DecimalFormat("0.00").format(speed); txtvPlaybackSpeed.setText(speedStr); butPlaybackSpeed.setSpeed(speed); - butPlaybackSpeed.setAlpha(controller.canSetPlaybackSpeed() ? 1.0f : 0.5f); butPlaybackSpeed.setVisibility(View.VISIBLE); txtvPlaybackSpeed.setVisibility(View.VISIBLE); } @@ -298,16 +297,17 @@ public class AudioPlayerFragment extends Fragment implements disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); } }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> updateUi((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> updateUi(null)); + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> updateUi((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> updateUi(null)); } private PlaybackController newPlaybackController() { @@ -354,8 +354,8 @@ public class AudioPlayerFragment extends Fragment implements } @Override - public ImageButton getPlayButton() { - return butPlay; + protected void updatePlayButtonShowsPlay(boolean showPlay) { + butPlay.setIsShowPlay(showPlay); } @Override @@ -364,11 +364,6 @@ public class AudioPlayerFragment extends Fragment implements } @Override - public void onShutdownNotification() { - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } - - @Override public void onPlaybackEnd() { ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); } @@ -377,11 +372,6 @@ public class AudioPlayerFragment extends Fragment implements public void onPlaybackSpeedChange() { updatePlaybackSpeedButton(getMedia()); } - - @Override - public void onSetSpeedAbilityChanged() { - updatePlaybackSpeedButton(getMedia()); - } }; } @@ -389,8 +379,10 @@ public class AudioPlayerFragment extends Fragment implements if (controller == null) { return; } - updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + duration = controller.getDuration(); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), duration)); updatePlaybackSpeedButton(media); + setChapterDividers(media); setupOptionsMenu(media); } @@ -433,6 +425,7 @@ public class AudioPlayerFragment extends Fragment implements int currentPosition = converter.convert(event.getPosition()); int duration = converter.convert(event.getDuration()); int remainingTime = converter.convert(Math.max(event.getDuration() - event.getPosition(), 0)); + currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), currentPosition); Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); if (currentPosition == PlaybackService.INVALID_TIME || duration == PlaybackService.INVALID_TIME) { Log.w(TAG, "Could not react to position observer update because of invalid time"); @@ -445,8 +438,11 @@ public class AudioPlayerFragment extends Fragment implements } else { txtvLength.setText(Converter.getDurationStringLong(duration)); } - float progress = ((float) event.getPosition()) / event.getDuration(); - sbPosition.setProgress((int) (progress * sbPosition.getMax())); + + if (!sbPosition.isPressed()) { + float progress = ((float) event.getPosition()) / event.getDuration(); + sbPosition.setProgress((int) (progress * sbPosition.getMax())); + } } @Subscribe(threadMode = ThreadMode.MAIN) @@ -459,11 +455,28 @@ public class AudioPlayerFragment extends Fragment implements if (controller == null || txtvLength == null) { return; } + if (fromUser) { float prog = progress / ((float) seekBar.getMax()); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); int position = converter.convert((int) (prog * controller.getDuration())); - txtvSeek.setText(Converter.getDurationStringLong(position)); + int newChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), position); + if (newChapterIndex > -1) { + if (!sbPosition.isPressed() && currentChapterIndex != newChapterIndex) { + currentChapterIndex = newChapterIndex; + position = (int) controller.getMedia().getChapters().get(currentChapterIndex).getStart(); + seekedToChapterStart = true; + controller.seekTo(position); + updateUi(controller.getMedia()); + sbPosition.highlightCurrentChapter(); + } + txtvSeek.setText(controller.getMedia().getChapters().get(newChapterIndex).getTitle() + + "\n" + Converter.getDurationStringLong(position)); + } else { + txtvSeek.setText(Converter.getDurationStringLong(position)); + } + } else if (duration != controller.getDuration()) { + updateUi(controller.getMedia()); } } @@ -482,8 +495,12 @@ public class AudioPlayerFragment extends Fragment implements @Override public void onStopTrackingTouch(SeekBar seekBar) { if (controller != null) { - float prog = seekBar.getProgress() / ((float) seekBar.getMax()); - controller.seekTo((int) (prog * controller.getDuration())); + if (seekedToChapterStart) { + seekedToChapterStart = false; + } else { + float prog = seekBar.getProgress() / ((float) seekBar.getMax()); + controller.seekTo((int) (prog * controller.getDuration())); + } } cardViewSeek.setScaleX(1f); cardViewSeek.setScaleY(1f); @@ -557,14 +574,13 @@ public class AudioPlayerFragment extends Fragment implements @Override public Fragment createFragment(int position) { Log.d(TAG, "getItem(" + position + ")"); + switch (position) { case POS_COVER: return new CoverFragment(); - case POS_SHOWNOTES: - return new ItemDescriptionFragment(); default: - case POS_CHAPTERS: - return new ChaptersFragment(); + case POS_DESCRIPTION: + return new ItemDescriptionFragment(); } } @@ -573,4 +589,21 @@ public class AudioPlayerFragment extends Fragment implements return NUM_CONTENT_FRAGMENTS; } } + + public void scrollToPage(int page, boolean smoothScroll) { + if (pager == null) { + return; + } + + pager.setCurrentItem(page, smoothScroll); + + Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + POS_DESCRIPTION); + if (visibleChild instanceof ItemDescriptionFragment) { + ((ItemDescriptionFragment) visibleChild).scrollToTop(); + } + } + + public void scrollToPage(int page) { + scrollToPage(page, false); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index b578a603f..de14f220e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -1,48 +1,65 @@ package de.danoeh.antennapod.fragment; +import android.app.Dialog; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; -import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.view.EmptyViewHandler; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.playback.Playable; import io.reactivex.Maybe; 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; -public class ChaptersFragment extends Fragment { - private static final String TAG = "ChaptersFragment"; +public class ChaptersFragment extends AppCompatDialogFragment { + public static final String TAG = "ChaptersFragment"; private ChaptersListAdapter adapter; private PlaybackController controller; private Disposable disposable; private int focusedChapter = -1; private Playable media; private LinearLayoutManager layoutManager; + private ProgressBar progressBar; - @Nullable + @NonNull @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.simple_list_fragment, container, false); + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.chapters_label)) + .setView(onCreateView(getLayoutInflater())) + .setNegativeButton(getString(R.string.cancel_label), null) //dismisses + .create(); + } + + + public View onCreateView(@NonNull LayoutInflater inflater) { + View root = inflater.inflate(R.layout.simple_list_fragment, null, false); root.findViewById(R.id.toolbar).setVisibility(View.GONE); RecyclerView recyclerView = root.findViewById(R.id.recyclerView); + progressBar = root.findViewById(R.id.progLoading); layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), @@ -53,16 +70,16 @@ public class ChaptersFragment extends Fragment { controller.playPause(); } Chapter chapter = adapter.getItem(pos); - controller.seekToChapter(chapter); + controller.seekTo((int) chapter.getStart()); updateChapterSelection(pos); }); recyclerView.setAdapter(adapter); - EmptyViewHandler emptyView = new EmptyViewHandler(getContext()); - emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.attr.ic_bookmark); - emptyView.setTitle(R.string.no_chapters_head_label); - emptyView.setMessage(R.string.no_chapters_label); + progressBar.setVisibility(View.VISIBLE); + + RelativeLayout.LayoutParams wrapHeight = new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + recyclerView.setLayoutParams(wrapHeight); return root; } @@ -118,7 +135,7 @@ public class ChaptersFragment extends Fragment { disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { - media.loadChapterMarks(getContext()); + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); @@ -136,8 +153,12 @@ public class ChaptersFragment extends Fragment { if (adapter == null) { return; } + if (media.getChapters() != null && media.getChapters().size() <= 0) { + dismiss(); + } else { + progressBar.setVisibility(View.GONE); + } adapter.setMedia(media); - ((AudioPlayerFragment) getParentFragment()).setHasChapters(adapter.getItemCount() > 0); int positionOfCurrentChapter = getCurrentChapter(media); updateChapterSelection(positionOfCurrentChapter); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index 3519a34b4..613216610 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -22,7 +22,7 @@ import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -146,7 +146,7 @@ public class CompletedDownloadsFragment extends Fragment { private void addEmptyView() { emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.attr.av_download); + emptyView.setIcon(R.drawable.ic_download); emptyView.setTitle(R.string.no_comp_downloads_head_label); emptyView.setMessage(R.string.no_comp_downloads_label); emptyView.attachToRecyclerView(recyclerView); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 5cb44655e..0dd97098e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -1,7 +1,15 @@ package de.danoeh.antennapod.fragment; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; +import android.graphics.ColorFilter; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; @@ -10,34 +18,47 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.Space; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; import androidx.fragment.app.Fragment; + import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.snackbar.Snackbar; + +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; -import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.playback.Playable; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; /** * Displays the cover and the title of a FeedItem. @@ -51,9 +72,16 @@ public class CoverFragment extends Fragment { private TextView txtvPodcastTitle; private TextView txtvEpisodeTitle; private ImageView imgvCover; + private LinearLayout openDescription; + private Space counterweight; + private Space spacer; + private ImageButton butPrevChapter; + private ImageButton butNextChapter; + private LinearLayout episodeDetails; + private LinearLayout chapterControl; private PlaybackController controller; private Disposable disposable; - private int displayedChapterIndex = -2; + private int displayedChapterIndex = -1; private Playable media; @Override @@ -64,7 +92,30 @@ public class CoverFragment extends Fragment { txtvPodcastTitle = root.findViewById(R.id.txtvPodcastTitle); txtvEpisodeTitle = root.findViewById(R.id.txtvEpisodeTitle); imgvCover = root.findViewById(R.id.imgvCover); + episodeDetails = root.findViewById(R.id.episode_details); + final ImageView descriptionIcon = root.findViewById(R.id.description_icon); + chapterControl = root.findViewById(R.id.chapterButton); + butPrevChapter = root.findViewById(R.id.butPrevChapter); + butNextChapter = root.findViewById(R.id.butNextChapter); + imgvCover.setOnClickListener(v -> onPlayPause()); + openDescription = root.findViewById(R.id.openDescription); + counterweight = root.findViewById(R.id.counterweight); + spacer = root.findViewById(R.id.details_spacer); + View.OnClickListener scrollToDesc = view -> + ((AudioPlayerFragment) requireParentFragment()).scrollToPage(AudioPlayerFragment.POS_DESCRIPTION, true); + openDescription.setOnClickListener(scrollToDesc); + ColorFilter colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + txtvPodcastTitle.getCurrentTextColor(), BlendModeCompat.SRC_IN); + butNextChapter.setColorFilter(colorFilter); + butPrevChapter.setColorFilter(colorFilter); + descriptionIcon.setColorFilter(colorFilter); + ChaptersFragment chaptersFragment = new ChaptersFragment(); + chapterControl.setOnClickListener(v -> + chaptersFragment.show(getChildFragmentManager(), ChaptersFragment.TAG)); + butPrevChapter.setOnClickListener(v -> seekToPrevChapter()); + butNextChapter.setOnClickListener(v -> seekToNextChapter()); + return root; } @@ -80,6 +131,7 @@ public class CoverFragment extends Fragment { disposable = Maybe.<Playable>create(emitter -> { Playable media = controller.getMedia(); if (media != null) { + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); @@ -99,9 +151,102 @@ public class CoverFragment extends Fragment { + "・" + "\u00A0" + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); + Intent openFeed = MainActivity.getIntentToOpenFeed(requireContext(), ((FeedMedia) media).getItem().getFeedId()); + txtvPodcastTitle.setOnClickListener(v -> startActivity(openFeed)); + txtvPodcastTitle.setOnLongClickListener(v -> copyText(media.getFeedTitle())); txtvEpisodeTitle.setText(media.getEpisodeTitle()); - displayedChapterIndex = -2; // Force refresh - displayCoverImage(media.getPosition()); + txtvEpisodeTitle.setOnLongClickListener(v -> copyText(media.getEpisodeTitle())); + txtvEpisodeTitle.setOnClickListener(v -> { + int lines = txtvEpisodeTitle.getLineCount(); + int animUnit = 1500; + if (lines > txtvEpisodeTitle.getMaxLines()) { + ObjectAnimator verticalMarquee = ObjectAnimator.ofInt( + txtvEpisodeTitle, "scrollY", 0, txtvEpisodeTitle.getHeight()) + .setDuration(lines * animUnit); + ObjectAnimator fadeOut = ObjectAnimator.ofFloat( + txtvEpisodeTitle, "alpha", 0); + fadeOut.setStartDelay(animUnit); + fadeOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + txtvEpisodeTitle.scrollTo(0, 0); + } + }); + ObjectAnimator fadeBackIn = ObjectAnimator.ofFloat( + txtvEpisodeTitle, "alpha", 1); + AnimatorSet set = new AnimatorSet(); + set.playSequentially(verticalMarquee, fadeOut, fadeBackIn); + set.start(); + } + }); + + displayedChapterIndex = -1; + refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())); //calls displayCoverImage + updateChapterControlVisibility(); + } + + private void updateChapterControlVisibility() { + if (media.getChapters() != null) { + boolean chapterControlVisible = media.getChapters().size() > 0; + int newVisibility = chapterControlVisible ? View.VISIBLE : View.GONE; + if (chapterControl.getVisibility() != newVisibility) { + chapterControl.setVisibility(newVisibility); + ObjectAnimator.ofFloat(chapterControl, + "alpha", + chapterControlVisible ? 0 : 1, + chapterControlVisible ? 1 : 0) + .start(); + } + } + } + + private void refreshChapterData(int chapterIndex) { + if (chapterIndex > -1) { + if (media.getPosition() > media.getDuration() || chapterIndex >= media.getChapters().size() - 1) { + displayedChapterIndex = media.getChapters().size() - 1; + butNextChapter.setVisibility(View.INVISIBLE); + } else { + displayedChapterIndex = chapterIndex; + butNextChapter.setVisibility(View.VISIBLE); + } + } + + displayCoverImage(); + } + + private Chapter getCurrentChapter() { + if (media == null || media.getChapters() == null || displayedChapterIndex == -1) { + return null; + } + return media.getChapters().get(displayedChapterIndex); + } + + private void seekToPrevChapter() { + Chapter curr = getCurrentChapter(); + + if (controller == null || curr == null || displayedChapterIndex == -1) { + return; + } + + if (displayedChapterIndex < 1) { + controller.seekTo(0); + } else if ((controller.getPosition() - 10000 * controller.getCurrentPlaybackSpeedMultiplier()) + < curr.getStart()) { + refreshChapterData(displayedChapterIndex - 1); + controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); + } else { + controller.seekTo((int) curr.getStart()); + } + } + + private void seekToNextChapter() { + if (controller == null || media == null || media.getChapters() == null + || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media.getChapters().size()) { + return; + } + + refreshChapterData(displayedChapterIndex + 1); + controller.seekTo((int) media.getChapters().get(displayedChapterIndex).getStart()); } @Override @@ -139,22 +284,18 @@ public class CoverFragment extends Fragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(PlaybackPositionEvent event) { - if (media == null) { - return; + int newChapterIndex = ChapterUtils.getCurrentChapterIndex(media, event.getPosition()); + if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) { + refreshChapterData(newChapterIndex); } - displayCoverImage(event.getPosition()); } - private void displayCoverImage(int position) { - int chapter = ChapterUtils.getCurrentChapterIndex(media, position); - if (chapter != displayedChapterIndex) { - displayedChapterIndex = chapter; - - RequestOptions options = new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); + private void displayCoverImage() { + RequestOptions options = new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .transforms(new FitCenter(), + new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); RequestBuilder<Drawable> cover = Glide.with(this) .load(media.getImageLocation()) @@ -163,16 +304,16 @@ public class CoverFragment extends Fragment { .apply(options)) .apply(options); - if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) { - cover.into(imgvCover); - } else { - Glide.with(this) - .load(EmbeddedChapterImage.getModelFor(media, chapter)) - .apply(options) - .thumbnail(cover) - .error(cover) - .into(imgvCover); - } + if (displayedChapterIndex == -1 || media == null || media.getChapters() == null + || TextUtils.isEmpty(media.getChapters().get(displayedChapterIndex).getImageUrl())) { + cover.into(imgvCover); + } else { + Glide.with(this) + .load(EmbeddedChapterImage.getModelFor(media, displayedChapterIndex)) + .apply(options) + .thumbnail(cover) + .error(cover) + .into(imgvCover); } } @@ -196,6 +337,10 @@ public class CoverFragment extends Fragment { LinearLayout.LayoutParams textParams = (LinearLayout.LayoutParams) textContainer.getLayoutParams(); double ratio = (float) newConfig.screenHeightDp / (float) newConfig.screenWidthDp; + boolean spacerVisible = true; + ViewGroup detailsParent = (ViewGroup) getView(); + int detailsWidth = ViewGroup.LayoutParams.MATCH_PARENT; + if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { double percentageWidth = 0.8; if (ratio <= SIXTEEN_BY_NINE) { @@ -217,6 +362,26 @@ public class CoverFragment extends Fragment { textParams.weight = 1; imgvCover.setLayoutParams(params); } + + spacerVisible = false; + detailsParent = textContainer; + detailsWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + } + + if (displayedChapterIndex == -1) { + detailsWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + } + + spacer.setVisibility(spacerVisible ? View.VISIBLE : View.GONE); + counterweight.setVisibility(spacerVisible ? View.VISIBLE : View.GONE); + LinearLayout.LayoutParams wrapHeight = + new LinearLayout.LayoutParams(detailsWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + episodeDetails.setLayoutParams(wrapHeight); + getView().findViewById(R.id.vertical_divider).setVisibility(spacerVisible ? View.GONE : View.VISIBLE); + + if (episodeDetails.getParent() != detailsParent) { + ((ViewGroup) episodeDetails.getParent()).removeView(episodeDetails); + detailsParent.addView(episodeDetails); } } @@ -226,4 +391,15 @@ public class CoverFragment extends Fragment { } controller.playPause(); } + + private boolean copyText(String text) { + ClipboardManager clipboardManager = ContextCompat.getSystemService(requireContext(), ClipboardManager.class); + if (clipboardManager != null) { + clipboardManager.setPrimaryClip(ClipData.newPlainText("AntennaPod", text)); + } + ((MainActivity) requireActivity()).showSnackbarAbovePlayer( + getResources().getString(R.string.copied_to_clipboard), + Snackbar.LENGTH_SHORT); + return true; + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index 2e11ea4ec..1f6067125 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -5,36 +5,36 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.ListFragment; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.ListFragment; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DownloadLogAdapter; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloadLogEvent; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -44,6 +44,9 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.ArrayList; +import java.util.List; + /** * Shows the download log */ @@ -52,6 +55,7 @@ public class DownloadLogFragment extends ListFragment { private static final String TAG = "DownloadLogFragment"; private List<DownloadStatus> downloadLog = new ArrayList<>(); + private List<Downloader> runningDownloads = new ArrayList<>(); private DownloadLogAdapter adapter; private Disposable disposable; @@ -60,7 +64,7 @@ public class DownloadLogFragment extends ListFragment { @Override public void onStart() { super.onStart(); - loadItems(); + loadDownloadLog(); } @Override @@ -79,14 +83,15 @@ public class DownloadLogFragment extends ListFragment { lv.setClipToPadding(false); final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); lv.setPadding(0, vertPadding, 0, vertPadding); + setListShown(true); EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.attr.av_download); + emptyView.setIcon(R.drawable.ic_download); emptyView.setTitle(R.string.no_log_downloads_head_label); emptyView.setMessage(R.string.no_log_downloads_label); emptyView.attachToListView(getListView()); - adapter = new DownloadLogAdapter(getActivity(), itemAccess); + adapter = new DownloadLogAdapter(getActivity(), this); setListAdapter(adapter); EventBus.getDefault().register(this); } @@ -97,70 +102,64 @@ public class DownloadLogFragment extends ListFragment { super.onDestroyView(); } - private void onFragmentLoaded() { - setListShown(true); - adapter.notifyDataSetChanged(); - ((PagedToolbarFragment) getParentFragment()).invalidateOptionsMenuIfActive(this); - } - @Override public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) { super.onListItemClick(l, v, position, id); - DownloadStatus status = adapter.getItem(position); - String url = "unknown"; - String message = getString(R.string.download_successful); - if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - if (media != null) { - url = media.getDownload_url(); - } - } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - Feed feed = DBReader.getFeed(status.getFeedfileId()); - if (feed != null) { - url = feed.getDownload_url(); - } - } - - if (!status.isSuccessful()) { - message = status.getReasonDetailed(); - } - - String messageFull = getString(R.string.download_error_details_message, message, url); - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setTitle(R.string.download_error_details); - builder.setMessage(messageFull); - builder.setPositiveButton(android.R.string.ok, null); - builder.setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> { - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(getString(R.string.download_error_details), messageFull); - clipboard.setPrimaryClip(clip); - ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT); - }); - Dialog dialog = builder.show(); - ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); - } + Object item = adapter.getItem(position); + if (item instanceof Downloader) { + DownloadRequest downloadRequest = ((Downloader) item).getDownloadRequest(); + DownloadRequester.getInstance().cancelDownload(getActivity(), downloadRequest.getSource()); - private final DownloadLogAdapter.ItemAccess itemAccess = new DownloadLogAdapter.ItemAccess() { + if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && UserPreferences.isEnableAutodownload()) { + FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId()); + DBWriter.setFeedItemAutoDownload(media.getItem(), false); - @Override - public int getCount() { - return downloadLog.size(); - } + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_SHORT); + } + } else if (item instanceof DownloadStatus) { + DownloadStatus status = (DownloadStatus) item; + String url = "unknown"; + String message = getString(R.string.download_successful); + if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); + if (media != null) { + url = media.getDownload_url(); + } + } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + Feed feed = DBReader.getFeed(status.getFeedfileId()); + if (feed != null) { + url = feed.getDownload_url(); + } + } - @Override - public DownloadStatus getItem(int position) { - if (0 <= position && position < downloadLog.size()) { - return downloadLog.get(position); - } else { - return null; + if (!status.isSuccessful()) { + message = status.getReasonDetailed(); } + + String messageFull = getString(R.string.download_error_details_message, message, url); + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(R.string.download_error_details); + builder.setMessage(messageFull); + builder.setPositiveButton(android.R.string.ok, null); + builder.setNeutralButton(R.string.copy_to_clipboard, (dialog, which) -> { + ClipboardManager clipboard = (ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.download_error_details), messageFull); + clipboard.setPrimaryClip(clip); + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT); + }); + Dialog dialog = builder.show(); + ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); } - }; + } @Subscribe public void onDownloadLogChanged(DownloadLogEvent event) { - loadItems(); + loadDownloadLog(); } @Override @@ -172,20 +171,16 @@ public class DownloadLogFragment extends ListFragment { @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (!super.onOptionsItemSelected(item)) { - switch (item.getItemId()) { - case R.id.clear_logs_item: - DBWriter.clearDownloadLog(); - return true; - case R.id.refresh_item: - AutoUpdateManager.runImmediate(requireContext()); - return true; - default: - return false; - } - } else { + if (super.onOptionsItemSelected(item)) { + return true; + } else if (item.getItemId() == R.id.clear_logs_item) { + DBWriter.clearDownloadLog(); + return true; + } else if (item.getItemId() == R.id.refresh_item) { + AutoUpdateManager.runImmediate(requireContext()); return true; } + return false; } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) @@ -196,10 +191,18 @@ public class DownloadLogFragment extends ListFragment { } } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEvent(DownloadEvent event) { + Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); + DownloaderUpdate update = event.update; + runningDownloads = update.downloaders; + adapter.setRunningDownloads(runningDownloads); + } + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); - private void loadItems() { + private void loadDownloadLog() { if (disposable != null) { disposable.dispose(); } @@ -209,7 +212,8 @@ public class DownloadLogFragment extends ListFragment { .subscribe(result -> { if (result != null) { downloadLog = result; - onFragmentLoaded(); + adapter.setDownloadLog(downloadLog); + ((PagedToolbarFragment) getParentFragment()).invalidateOptionsMenuIfActive(this); } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java index 5c83cee57..bc3884b37 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java @@ -31,10 +31,9 @@ public class DownloadsFragment extends PagedToolbarFragment { private static final String PREF_LAST_TAB_POSITION = "tab_position"; private static final String KEY_UP_ARROW = "up_arrow"; - public static final int POS_RUNNING = 0; - private static final int POS_COMPLETED = 1; - public static final int POS_LOG = 2; - private static final int TOTAL_COUNT = 3; + private static final int POS_COMPLETED = 0; + public static final int POS_LOG = 1; + private static final int TOTAL_COUNT = 2; private ViewPager2 viewPager; private TabLayout tabLayout; @@ -64,9 +63,6 @@ public class DownloadsFragment extends PagedToolbarFragment { tabLayout = root.findViewById(R.id.sliding_tabs); new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { switch (position) { - case POS_RUNNING: - tab.setText(R.string.downloads_running_label); - break; case POS_COMPLETED: tab.setText(R.string.downloads_completed_label); break; @@ -121,8 +117,6 @@ public class DownloadsFragment extends PagedToolbarFragment { @Override public Fragment createFragment(int position) { switch (position) { - case POS_RUNNING: - return new RunningDownloadsFragment(); case POS_COMPLETED: return new CompletedDownloadsFragment(); default: diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 39f935bbe..b43ae97a4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -40,7 +40,7 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -216,7 +216,7 @@ public abstract class EpisodesListFragment extends Fragment { emptyView = new EmptyViewHandler(getContext()); emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.attr.feed); + emptyView.setIcon(R.drawable.ic_feed); emptyView.setTitle(R.string.no_all_episodes_head_label); emptyView.setMessage(R.string.no_all_episodes_label); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 4efba9277..8e070738c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -6,7 +6,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; @@ -17,13 +16,15 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.event.ServiceEvent; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlayerStatus; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.view.PlayButton; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -40,7 +41,7 @@ public class ExternalPlayerFragment extends Fragment { private ImageView imgvCover; private TextView txtvTitle; - private ImageButton butPlay; + private PlayButton butPlay; private TextView feedName; private ProgressBar progressBar; private PlaybackController controller; @@ -103,8 +104,8 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public ImageButton getPlayButton() { - return butPlay; + protected void updatePlayButtonShowsPlay(boolean showPlay) { + butPlay.setIsShowPlay(showPlay); } @Override @@ -113,11 +114,6 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public void onShutdownNotification() { - ((MainActivity) getActivity()).setPlayerVisible(false); - } - - @Override public void onPlaybackEnd() { ((MainActivity) getActivity()).setPlayerVisible(false); } @@ -148,6 +144,13 @@ public class ExternalPlayerFragment extends Fragment { onPositionObserverUpdate(); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlaybackServiceChanged(ServiceEvent event) { + if (event.action == ServiceEvent.Action.SERVICE_SHUT_DOWN) { + ((MainActivity) getActivity()).setPlayerVisible(false); + } + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java index e1fd36731..986c417fd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java @@ -19,7 +19,7 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.FavoritesEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; @@ -55,7 +55,7 @@ public class FavoriteEpisodesFragment extends EpisodesListFragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = super.onCreateView(inflater, container, savedInstanceState); - emptyView.setIcon(R.attr.ic_unfav); + emptyView.setIcon(R.drawable.ic_star); emptyView.setTitle(R.string.no_fav_episodes_head_label); emptyView.setMessage(R.string.no_fav_episodes_label); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index 25ab925eb..da7e7e633 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -13,6 +13,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatDrawableManager; import androidx.appcompat.widget.Toolbar; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; @@ -33,10 +34,14 @@ import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.snackbar.Snackbar; import com.joanzapata.iconify.Iconify; + +import org.apache.commons.lang3.StringUtils; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedFunding; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.storage.DBReader; @@ -45,7 +50,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.StatisticsItem; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.fragment.preferences.StatisticsFragment; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; @@ -58,6 +62,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -80,6 +86,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic private TextView txtvPodcastTime; private TextView txtvPodcastSpace; private TextView txtvPodcastEpisodeCount; + private TextView txtvFundingUrl; + private TextView lblSupport; private Button btnvOpenStatistics; private TextView txtvUrl; private TextView txtvAuthorHeader; @@ -129,9 +137,9 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic @Override protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.visit_website_item) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.location_web_site)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_web)); toolbar.getMenu().findItem(R.id.share_parent) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_share)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_share)); } }; iconTintManager.updateTint(); @@ -155,6 +163,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic txtvPodcastTime = root.findViewById(R.id.txtvPodcastTime); btnvOpenStatistics = root.findViewById(R.id.btnvOpenStatistics); txtvUrl = root.findViewById(R.id.txtvUrl); + lblSupport = root.findViewById(R.id.lblSupport); + txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl); txtvUrl.setOnClickListener(copyUrlToClipboard); @@ -222,6 +232,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic .into(imgvBackground); txtvTitle.setText(feed.getTitle()); + txtvTitle.setMaxLines(6); String description = HtmlToPlainText.getPlainText(feed.getDescription()); @@ -232,6 +243,29 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic } txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}"); + + if (feed.getPaymentLinks() == null || feed.getPaymentLinks().size() == 0) { + lblSupport.setVisibility(View.GONE); + txtvFundingUrl.setVisibility(View.GONE); + } else { + lblSupport.setVisibility(View.VISIBLE); + ArrayList<FeedFunding> fundingList = feed.getPaymentLinks(); + StringBuilder str = new StringBuilder(); + HashSet<String> seen = new HashSet<String>(); + for (FeedFunding funding : fundingList) { + if (seen.contains(funding.url)) { + continue; + } + seen.add(funding.url); + str.append(funding.content.isEmpty() + ? getContext().getResources().getString(R.string.support_podcast) + : funding.content).append(" ").append(funding.url); + str.append("\n"); + } + str = new StringBuilder(StringUtils.trim(str.toString())); + txtvFundingUrl.setText(str.toString()); + } + Iconify.addIcons(txtvUrl); refreshToolbarState(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index c86fdc070..7aa4d5094 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -20,6 +20,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatDrawableManager; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; @@ -42,10 +43,10 @@ import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.service.download.DownloadService; @@ -56,8 +57,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.Optional; -import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.FilterDialog; @@ -101,6 +100,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private ImageView imgvCover; private TextView txtvInformation; private TextView txtvAuthor; + private TextView txtvUpdatesDisabled; private ImageButton butShowInfo; private ImageButton butShowSettings; private View header; @@ -167,6 +167,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem butShowSettings = root.findViewById(R.id.butShowSettings); txtvInformation = root.findViewById(R.id.txtvInformation); txtvFailure = root.findViewById(R.id.txtvFailure); + txtvUpdatesDisabled = root.findViewById(R.id.txtvUpdatesDisabled); header = root.findViewById(R.id.headerContainer); AppBarLayout appBar = root.findViewById(R.id.appBar); CollapsingToolbarLayout collapsingToolbar = root.findViewById(R.id.collapsing_toolbar); @@ -175,13 +176,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem @Override protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.sort_items) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_sort)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_sort)); toolbar.getMenu().findItem(R.id.filter_items) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_filter)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_filter)); toolbar.getMenu().findItem(R.id.refresh_item) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.navigation_refresh)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_refresh)); toolbar.getMenu().findItem(R.id.action_search) - .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.action_search)); + .setIcon(AppCompatDrawableManager.get().getDrawable(themedContext, R.drawable.ic_search)); } }; iconTintManager.updateTint(); @@ -457,6 +458,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } else { txtvFailure.setVisibility(View.GONE); } + if (!feed.getPreferences().getKeepUpdated()) { + txtvUpdatesDisabled.setText("{md-pause-circle-outline} " + this.getString(R.string.updates_disabled_label)); + Iconify.addIcons(txtvUpdatesDisabled); + txtvUpdatesDisabled.setVisibility(View.VISIBLE); + } else { + txtvUpdatesDisabled.setVisibility(View.GONE); + } txtvTitle.setText(feed.getTitle()); txtvAuthor.setText(feed.getAuthor()); if (feed.getItemFilter() != null) { @@ -549,27 +557,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem disposable = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - feed = result.orElse(null); - refreshHeaderView(); - displayList(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe( + result -> { + feed = result; + refreshHeaderView(); + displayList(); + }, error -> { + feed = null; + refreshHeaderView(); + displayList(); + Log.e(TAG, Log.getStackTraceString(error)); + }); } - @NonNull - private Optional<Feed> loadData() { - Feed feed = DBReader.getFeed(feedID); - if (feed != null && feed.getItemFilter() != null) { - DBReader.loadAdditionalFeedItemListData(feed.getItems()); - FeedItemFilter filter = feed.getItemFilter(); - feed.setItems(filter.filter(feed.getItems())); + @Nullable + private Feed loadData() { + Feed feed = DBReader.getFeed(feedID, true); + if (feed == null) { + return null; } - if (feed != null && feed.getSortOrder() != null) { + DBReader.loadAdditionalFeedItemListData(feed.getItems()); + if (feed.getSortOrder() != null) { List<FeedItem> feedItems = feed.getItems(); FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); feed.setItems(feedItems); } - return Optional.ofNullable(feed); + return feed; } private static class FeedItemListAdapter extends EpisodeItemListAdapter { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index e24c89478..9e57497bf 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -20,16 +20,17 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent; import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent; import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedFilter; -import de.danoeh.antennapod.core.feed.FeedPreferences; -import de.danoeh.antennapod.core.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedFilter; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; 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.dialog.AuthenticationDialog; import de.danoeh.antennapod.dialog.EpisodeFilterDialog; import de.danoeh.antennapod.dialog.FeedPreferenceSkipDialog; +import de.danoeh.antennapod.dialog.TagSettingsDialog; import io.reactivex.Maybe; import io.reactivex.MaybeOnSubscribe; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -41,7 +42,7 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; -import static de.danoeh.antennapod.core.feed.FeedPreferences.SPEED_USE_GLOBAL; +import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; public class FeedSettingsFragment extends Fragment { private static final String TAG = "FeedSettingsFragment"; @@ -105,6 +106,7 @@ public class FeedSettingsFragment extends Fragment { private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory"; private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed"; private static final String PREF_AUTO_SKIP = "feedAutoSkip"; + private static final String PREF_TAGS = "tags"; private static final DecimalFormat SPEED_FORMAT = new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US)); @@ -160,6 +162,7 @@ public class FeedSettingsFragment extends Fragment { setupPlaybackSpeedPreference(); setupFeedAutoSkipPreference(); setupEpisodeNotificationPreference(); + setupTags(); updateAutoDeleteSummary(); updateVolumeReductionValue(); @@ -395,6 +398,13 @@ public class FeedSettingsFragment extends Fragment { } } + private void setupTags() { + findPreference(PREF_TAGS).setOnPreferenceClickListener(preference -> { + TagSettingsDialog.newInstance(feedPreferences).show(getChildFragmentManager(), TagSettingsDialog.TAG); + return true; + }); + } + private void setupEpisodeNotificationPreference() { SwitchPreferenceCompat pref = findPreference("episodeNotification"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 8ac7b941a..518450f92 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -8,11 +8,15 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; + import androidx.fragment.app.Fragment; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.view.ShownotesWebView; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -84,12 +88,19 @@ public class ItemDescriptionFragment extends Fragment { } webViewLoader = Maybe.<String>create(emitter -> { Playable media = controller.getMedia(); - if (media != null) { - Timeline timeline = new Timeline(getActivity(), media); - emitter.onSuccess(timeline.processShownotes()); - } else { + if (media == null) { emitter.onComplete(); + return; + } + if (media instanceof FeedMedia) { + FeedMedia feedMedia = ((FeedMedia) media); + if (feedMedia.getItem() == null) { + feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); + } + DBReader.loadDescriptionOfFeedItem(feedMedia.getItem()); } + Timeline timeline = new Timeline(getActivity(), media.getDescription(), media.getDuration()); + emitter.onSuccess(timeline.processShownotes()); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -134,14 +145,18 @@ public class ItemDescriptionFragment extends Fragment { && id.equals(controller.getMedia().getIdentifier().toString()) && webvDescription != null) { Log.d(TAG, "Restored scroll Position: " + scrollY); - webvDescription.scrollTo(webvDescription.getScrollX(), - scrollY); + webvDescription.scrollTo(webvDescription.getScrollX(), scrollY); return true; } } return false; } + public void scrollToTop() { + webvDescription.scrollTo(0, 0); + savePreference(); + } + @Override public void onStart() { super.onStart(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 48eae9583..c83ed4722 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -6,7 +6,6 @@ import android.os.Bundle; import android.text.Layout; import android.text.TextUtils; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -46,8 +45,8 @@ import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UsageStatistics; @@ -57,6 +56,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; @@ -337,7 +337,7 @@ public class ItemFragment extends Fragment { txtvDuration.setContentDescription( Converter.getDurationStringLocalized(getContext(), media.getDuration())); } - if (media.isCurrentlyPlaying()) { + if (FeedItemUtil.isCurrentlyPlaying(media)) { actionButton1 = new PauseActionButton(item); } else if (item.getFeed().isLocalFeed()) { actionButton1 = new PlayLocalActionButton(item); @@ -357,15 +357,12 @@ public class ItemFragment extends Fragment { butAction1Text.setText(actionButton1.getLabel()); butAction1Text.setTransformationMethod(null); - TypedValue typedValue = new TypedValue(); - getContext().getTheme().resolveAttribute(actionButton1.getDrawable(), typedValue, true); - butAction1Icon.setImageResource(typedValue.resourceId); + butAction1Icon.setImageResource(actionButton1.getDrawable()); butAction1.setVisibility(actionButton1.getVisibility()); butAction2Text.setText(actionButton2.getLabel()); butAction2Text.setTransformationMethod(null); - getContext().getTheme().resolveAttribute(actionButton2.getDrawable(), typedValue, true); - butAction2Icon.setImageResource(typedValue.resourceId); + butAction2Icon.setImageResource(actionButton2.getDrawable()); butAction2.setVisibility(actionButton2.getVisibility()); } @@ -439,7 +436,9 @@ public class ItemFragment extends Fragment { FeedItem feedItem = DBReader.getFeedItem(itemId); Context context = getContext(); if (feedItem != null && context != null) { - Timeline t = new Timeline(context, feedItem); + int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE; + DBReader.loadDescriptionOfFeedItem(feedItem); + Timeline t = new Timeline(context, feedItem.getDescription(), duration); webviewData = t.processShownotes(); } return feedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java index 7b7a09082..d42300ca7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemPagerFragment.java @@ -21,7 +21,7 @@ import org.greenrobot.eventbus.ThreadMode; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import io.reactivex.Observable; 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 be74678d3..98ba59980 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -12,15 +12,16 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; +import androidx.core.util.Pair; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; @@ -30,10 +31,11 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.Feed; +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.RenameFeedDialog; @@ -46,12 +48,15 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; -public class NavDrawerFragment extends Fragment implements AdapterView.OnItemClickListener, - AdapterView.OnItemLongClickListener, SharedPreferences.OnSharedPreferenceChangeListener { +public class NavDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener { @VisibleForTesting public static final String PREF_LAST_FRAGMENT_TAG = "prefLastFragmentTag"; + private static final String PREF_OPEN_FOLDERS = "prefOpenFolders"; @VisibleForTesting public static final String PREF_NAME = "NavDrawerPrefs"; public static final String TAG = "NavDrawerFragment"; @@ -66,12 +71,13 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli NavListAdapter.SUBSCRIPTION_LIST_TAG }; - private DBReader.NavDrawerData navDrawerData; - private int selectedNavListIndex = -1; - private int position = -1; + private NavDrawerData navDrawerData; + private List<NavDrawerData.DrawerItem> flatItemList; + private NavDrawerData.DrawerItem contextPressedItem = null; private NavListAdapter navAdapter; private Disposable disposable; private ProgressBar progressBar; + private Set<String> openFolders = new HashSet<>(); @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @@ -79,40 +85,21 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli super.onCreateView(inflater, container, savedInstanceState); View root = inflater.inflate(R.layout.nav_list, container, false); + SharedPreferences preferences = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + openFolders = new HashSet<>(preferences.getStringSet(PREF_OPEN_FOLDERS, new HashSet<>())); // Must not modify + progressBar = root.findViewById(R.id.progressBar); - ListView navList = root.findViewById(R.id.nav_list); + RecyclerView navList = root.findViewById(R.id.nav_list); navAdapter = new NavListAdapter(itemAccess, getActivity()); + navAdapter.setHasStableIds(true); navList.setAdapter(navAdapter); - navList.setOnItemClickListener(this); - navList.setOnItemLongClickListener(this); - registerForContextMenu(navList); - updateSelection(); + navList.setLayoutManager(new LinearLayoutManager(getContext())); root.findViewById(R.id.nav_settings).setOnClickListener(v -> startActivity(new Intent(getActivity(), PreferenceActivity.class))); - getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .registerOnSharedPreferenceChangeListener(this); - return root; - } - private void updateSelection() { - String lastNavFragment = getLastNavFragment(getContext()); - int tagIndex = navAdapter.getTags().indexOf(lastNavFragment); - if (tagIndex >= 0) { - selectedNavListIndex = tagIndex; - } else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed - long feedId = Long.parseLong(lastNavFragment); - if (navDrawerData != null) { - List<Feed> feeds = navDrawerData.feeds; - for (int i = 0; i < feeds.size(); i++) { - if (feeds.get(i).getId() == feedId) { - selectedNavListIndex = navAdapter.getSubscriptionOffset() + i; - break; - } - } - } - } - navAdapter.notifyDataSetChanged(); + preferences.registerOnSharedPreferenceChangeListener(this); + return root; } @Override @@ -135,29 +122,27 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - if (v.getId() != R.id.nav_list) { - return; - } - AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; - int position = adapterInfo.position; - if (position < navAdapter.getSubscriptionOffset()) { - return; + if (contextPressedItem.type != NavDrawerData.DrawerItem.Type.FEED) { + return; // Should actually never happen because the context menu is not set up for other items } + MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.nav_feed_context, menu); - Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset()); - menu.setHeaderTitle(feed.getTitle()); + menu.setHeaderTitle(((NavDrawerData.FeedDrawerItem) contextPressedItem).feed.getTitle()); // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones! } @Override public boolean onContextItemSelected(@NonNull MenuItem item) { - final int position = this.position; - this.position = -1; // reset - if (position < 0) { - return false; + NavDrawerData.DrawerItem pressedItem = contextPressedItem; + contextPressedItem = null; + if (pressedItem != null && pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) pressedItem).feed, item); } - Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset()); + return false; + } + + private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) { switch (item.getItemId()) { case R.id.remove_all_new_flags_item: ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(), @@ -189,13 +174,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli return true; case R.id.remove_item: RemoveFeedDialog.show(getContext(), feed, () -> { - if (selectedNavListIndex == position) { - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null); - } else { - showMainActivity(EpisodesFragment.TAG); - } - } + ((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null); }); return true; default: @@ -203,12 +182,6 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli } } - private void showMainActivity(String tag) { - Intent intent = new Intent(getActivity(), MainActivity.class); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, tag); - startActivity(intent); - } - @Subscribe(threadMode = ThreadMode.MAIN) public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { loadData(); @@ -261,7 +234,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli }); builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { UserPreferences.setHiddenDrawerItems(hiddenDrawerItems); - updateSelection(); + navAdapter.notifyDataSetChanged(); // Update selection }); builder.setNegativeButton(R.string.cancel_label, null); builder.create().show(); @@ -270,25 +243,39 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() { @Override public int getCount() { - if (navDrawerData != null) { - return navDrawerData.feeds.size(); + if (flatItemList != null) { + return flatItemList.size(); } else { return 0; } } @Override - public Feed getItem(int position) { - if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) { - return navDrawerData.feeds.get(position); + public NavDrawerData.DrawerItem getItem(int position) { + if (flatItemList != null && 0 <= position && position < flatItemList.size()) { + return flatItemList.get(position); } else { return null; } } @Override - public int getSelectedItemIndex() { - return selectedNavListIndex; + public boolean isSelected(int position) { + String lastNavFragment = getLastNavFragment(getContext()); + if (position < navAdapter.getSubscriptionOffset()) { + return navAdapter.getFragmentTags().get(position).equals(lastNavFragment); + } else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed + long feedId = Long.parseLong(lastNavFragment); + if (navDrawerData != null) { + NavDrawerData.DrawerItem itemToCheck = flatItemList.get( + position - navAdapter.getSubscriptionOffset()); + if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) { + // When the same feed is displayed multiple times, it should be highlighted multiple times. + return ((NavDrawerData.FeedDrawerItem) itemToCheck).feed.getId() == feedId; + } + } + } + return false; } @Override @@ -312,11 +299,6 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli } @Override - public int getFeedCounter(long feedId) { - return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0; - } - - @Override public int getFeedCounterSum() { if (navDrawerData == null) { return 0; @@ -328,16 +310,81 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli return sum; } + @Override + public void onItemClick(int position) { + int viewType = navAdapter.getItemViewType(position); + if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { + if (position < navAdapter.getSubscriptionOffset()) { + String tag = navAdapter.getFragmentTags().get(position); + ((MainActivity) getActivity()).loadFragment(tag, null); + ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + int pos = position - navAdapter.getSubscriptionOffset(); + NavDrawerData.DrawerItem clickedItem = flatItemList.get(pos); + + if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) { + long feedId = ((NavDrawerData.FeedDrawerItem) clickedItem).feed.getId(); + ((MainActivity) getActivity()).loadFeedFragmentById(feedId, null); + ((MainActivity) getActivity()).getBottomSheet() + .setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) clickedItem); + if (openFolders.contains(folder.name)) { + openFolders.remove(folder.name); + } else { + openFolders.add(folder.name); + } + + getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putStringSet(PREF_OPEN_FOLDERS, openFolders) + .apply(); + + disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.items, 0)) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + flatItemList = result; + navAdapter.notifyDataSetChanged(); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + } + } else if (UserPreferences.getSubscriptionsFilter().isEnabled() + && navAdapter.showSubscriptionList) { + SubscriptionsFilterDialog.showDialog(requireContext()); + } + } + + @Override + public boolean onItemLongClick(int position) { + if (position < navAdapter.getFragmentTags().size()) { + showDrawerPreferencesDialog(); + return true; + } else { + contextPressedItem = flatItemList.get(position - navAdapter.getSubscriptionOffset()); + return false; + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + NavDrawerFragment.this.onCreateContextMenu(menu, v, menuInfo); + } }; private void loadData() { - disposable = Observable.fromCallable(DBReader::getNavDrawerData) + disposable = Observable.fromCallable( + () -> { + NavDrawerData data = DBReader.getNavDrawerData(); + return new Pair<>(data, makeFlatDrawerData(data.items, 0)); + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( result -> { - navDrawerData = result; - updateSelection(); // Selected item might be a feed + navDrawerData = result.first; + flatItemList = result.second; navAdapter.notifyDataSetChanged(); progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list }, error -> { @@ -346,45 +393,20 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli }); } - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - int viewType = parent.getAdapter().getItemViewType(position); - if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) { - if (position < navAdapter.getSubscriptionOffset()) { - String tag = navAdapter.getTags().get(position); - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).loadFragment(tag, null); - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - showMainActivity(tag); - } - } else { - int pos = position - navAdapter.getSubscriptionOffset(); - long feedId = navDrawerData.feeds.get(pos).getId(); - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).loadFeedFragmentById(feedId, null); - ((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - Intent intent = new Intent(getActivity(), MainActivity.class); - intent.putExtra(MainActivity.EXTRA_FEED_ID, feedId); - startActivity(intent); + private List<NavDrawerData.DrawerItem> makeFlatDrawerData(List<NavDrawerData.DrawerItem> items, int layer) { + List<NavDrawerData.DrawerItem> flatItems = new ArrayList<>(); + for (NavDrawerData.DrawerItem item : items) { + item.setLayer(layer); + flatItems.add(item); + if (item.type == NavDrawerData.DrawerItem.Type.FOLDER) { + NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) item); + folder.isOpen = openFolders.contains(folder.name); + if (folder.isOpen) { + flatItems.addAll(makeFlatDrawerData(((NavDrawerData.FolderDrawerItem) item).children, layer + 1)); } } - } else if (UserPreferences.getSubscriptionsFilter().isEnabled() - && navAdapter.showSubscriptionList) { - SubscriptionsFilterDialog.showDialog(requireContext()); - } - } - - @Override - public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { - if (position < navAdapter.getTags().size()) { - showDrawerPreferencesDialog(); - return true; - } else { - this.position = position; - return false; } + return flatItems; } public static void saveLastNavFragment(Context context, String tag) { @@ -409,8 +431,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PREF_LAST_FRAGMENT_TAG.equals(key)) { - updateSelection(); - navAdapter.notifyDataSetChanged(); + navAdapter.notifyDataSetChanged(); // Update selection } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index 1aa66dcbb..44b82ee19 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -12,7 +12,7 @@ import android.view.ViewGroup; import java.util.List; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java index 2ed26b1c0..f79bffabc 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PagedToolbarFragment.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.fragment; +import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; @@ -14,12 +15,12 @@ public abstract class PagedToolbarFragment extends Fragment { /** * Invalidate the toolbar menu if the current child fragment is visible. - * @param child The fragment, or null to force-refresh whatever the active fragment is. + * @param child The fragment to invalidate */ - void invalidateOptionsMenuIfActive(Fragment child) { + void invalidateOptionsMenuIfActive(@NonNull Fragment child) { Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem()); - if (visibleChild == child || child == null) { - child.onPrepareOptionsMenu(toolbar.getMenu()); + if (visibleChild == child) { + visibleChild.onPrepareOptionsMenu(toolbar.getMenu()); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index e97b7cd7f..c067af4e4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -21,7 +21,7 @@ import de.danoeh.antennapod.core.event.PlaybackHistoryEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; @@ -81,7 +81,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI progressBar = root.findViewById(R.id.progLoading); emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.attr.ic_history); + emptyView.setIcon(R.drawable.ic_history); emptyView.setTitle(R.string.no_history_head_label); emptyView.setMessage(R.string.no_history_label); emptyView.attachToRecyclerView(recyclerView); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index b92043c7d..1fda42d41 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -4,6 +4,8 @@ import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; @@ -19,6 +21,8 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; @@ -31,7 +35,7 @@ import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadService; @@ -40,7 +44,7 @@ import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; @@ -242,7 +246,6 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi private void refreshToolbarState() { MenuItemUtils.refreshLockItem(getActivity(), toolbar.getMenu()); boolean keepSorted = UserPreferences.isQueueKeepSorted(); - toolbar.getMenu().findItem(R.id.queue_lock).setVisible(!keepSorted); toolbar.getMenu().findItem(R.id.queue_sort_random).setVisible(!keepSorted); toolbar.getMenu().findItem(R.id.queue_keep_sorted).setChecked(keepSorted); @@ -441,6 +444,13 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool()); registerForContextMenu(recyclerView); + SwipeRefreshLayout swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); + swipeRefreshLayout.setOnRefreshListener(() -> { + AutoUpdateManager.runImmediate(requireContext()); + new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false), + getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms)); + }); + itemTouchHelper = new ItemTouchHelper( new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @@ -480,19 +490,13 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi final int position = viewHolder.getAdapterPosition(); Log.d(TAG, "remove(" + position + ")"); final FeedItem item = queue.get(position); - final boolean isRead = item.isPlayed(); - DBWriter.markItemPlayed(FeedItem.PLAYED, false, item.getId()); DBWriter.removeQueueItem(getActivity(), true, item); ((MainActivity) getActivity()).showSnackbarAbovePlayer( - item.hasMedia() ? R.string.marked_as_read_label : R.string.marked_as_read_no_media_label, + getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG) - .setAction(getString(R.string.undo), v -> { - DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false); - if (!isRead) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); - } - }); + .setAction(getString(R.string.undo), v -> + DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false)); } @Override @@ -527,7 +531,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi emptyView = new EmptyViewHandler(getContext()); emptyView.attachToRecyclerView(recyclerView); - emptyView.setIcon(R.attr.stat_playlist); + emptyView.setIcon(R.drawable.ic_playlist); emptyView.setTitle(R.string.no_items_header_label); emptyView.setMessage(R.string.no_items_label); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java deleted file mode 100644 index fc500a223..000000000 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ /dev/null @@ -1,154 +0,0 @@ -package de.danoeh.antennapod.fragment; - -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.ListFragment; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; -import android.widget.Toast; - -import de.danoeh.antennapod.activity.MainActivity; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.DownloadlistAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; -import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.view.EmptyViewHandler; -import org.greenrobot.eventbus.ThreadMode; - -/** - * Displays all running downloads and provides actions to cancel them - */ -public class RunningDownloadsFragment extends ListFragment { - - private static final String TAG = "RunningDownloadsFrag"; - - private DownloadlistAdapter adapter; - private List<Downloader> downloaderList = new ArrayList<>(); - - private boolean isUpdatingFeeds = false; - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // add padding - final ListView lv = getListView(); - lv.setClipToPadding(false); - final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); - lv.setPadding(0, vertPadding, 0, vertPadding); - - adapter = new DownloadlistAdapter(getActivity(), itemAccess); - setListAdapter(adapter); - - EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); - emptyView.setIcon(R.attr.av_download); - emptyView.setTitle(R.string.no_run_downloads_head_label); - emptyView.setMessage(R.string.no_run_downloads_label); - emptyView.attachToListView(getListView()); - - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - } - - @Override - public void onDestroy() { - super.onDestroy(); - setListAdapter(null); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - menu.findItem(R.id.clear_logs_item).setVisible(false); - menu.findItem(R.id.episode_actions).setVisible(false); - isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.refresh_item) { - AutoUpdateManager.runImmediate(requireContext()); - return true; - } - return false; - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) { - ((PagedToolbarFragment) getParentFragment()).invalidateOptionsMenuIfActive(this); - } - } - - private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = - () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(DownloadEvent event) { - Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - downloaderList = update.downloaders; - adapter.notifyDataSetChanged(); - } - - private final DownloadlistAdapter.ItemAccess itemAccess = new DownloadlistAdapter.ItemAccess() { - @Override - public int getCount() { - return downloaderList.size(); - } - - @Override - public Downloader getItem(int position) { - if (0 <= position && position < downloaderList.size()) { - return downloaderList.get(position); - } else { - return null; - } - } - - @Override - public void onSecondaryActionClick(Downloader downloader) { - DownloadRequest downloadRequest = downloader.getDownloadRequest(); - DownloadRequester.getInstance().cancelDownload(getActivity(), downloadRequest.getSource()); - - if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && UserPreferences.isEnableAutodownload()) { - FeedMedia media = DBReader.getFeedMedia(downloadRequest.getFeedfileId()); - DBWriter.setFeedItemAutoDownload(media.getItem(), false); - - ((MainActivity) getActivity()).showSnackbarAbovePlayer( - R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_SHORT); - } - } - }; -} 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 e791da1c6..c4fa0a3b1 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -28,8 +28,8 @@ import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.storage.FeedSearcher; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; @@ -133,7 +133,7 @@ public class SearchFragment extends Fragment { emptyViewHandler = new EmptyViewHandler(getContext()); emptyViewHandler.attachToRecyclerView(recyclerView); - emptyViewHandler.setIcon(R.attr.action_search); + emptyViewHandler.setIcon(R.drawable.ic_search); emptyViewHandler.setTitle(R.string.search_status_no_results); EventBus.getDefault().register(this); 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 9bbc03fba..367eb4aaf 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -28,6 +28,7 @@ import android.widget.TextView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.joanzapata.iconify.Iconify; +import java.util.List; import java.util.Locale; import java.util.concurrent.Callable; @@ -38,12 +39,13 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; @@ -68,6 +70,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private static final String PREFS = "SubscriptionFragment"; private static final String PREF_NUM_COLUMNS = "columns"; private static final String KEY_UP_ARROW = "up_arrow"; + private static final String ARGUMENT_FOLDER = "folder"; private static final int MIN_NUM_COLUMNS = 2; private static final int[] COLUMN_CHECKBOX_IDS = { @@ -77,21 +80,30 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem R.id.subscription_num_columns_5}; private GridView subscriptionGridLayout; - private DBReader.NavDrawerData navDrawerData; + private List<NavDrawerData.DrawerItem> listItems; private SubscriptionsAdapter subscriptionAdapter; private FloatingActionButton subscriptionAddButton; private ProgressBar progressBar; private EmptyViewHandler emptyView; private TextView feedsFilteredMsg; private Toolbar toolbar; + private String displayedFolder = null; - private int mPosition = -1; + private Feed selectedFeed = null; private boolean isUpdatingFeeds = false; private boolean displayUpArrow; private Disposable disposable; private SharedPreferences prefs; + public static SubscriptionFragment newInstance(String folderTitle) { + SubscriptionFragment fragment = new SubscriptionFragment(); + Bundle args = new Bundle(); + args.putString(ARGUMENT_FOLDER, folderTitle); + fragment.setArguments(args); + return fragment; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -119,6 +131,13 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem } refreshToolbarState(); + if (getArguments() != null) { + displayedFolder = getArguments().getString(ARGUMENT_FOLDER, null); + if (displayedFolder != null) { + toolbar.setTitle(displayedFolder); + } + } + subscriptionGridLayout = root.findViewById(R.id.subscriptions_grid); subscriptionGridLayout.setNumColumns(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns())); registerForContextMenu(subscriptionGridLayout); @@ -188,7 +207,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private void setupEmptyView() { emptyView = new EmptyViewHandler(getContext()); - emptyView.setIcon(R.attr.ic_folder); + emptyView.setIcon(R.drawable.ic_folder); emptyView.setTitle(R.string.no_subscriptions_head_label); emptyView.setMessage(R.string.no_subscriptions_label); emptyView.attachToListView(subscriptionGridLayout); @@ -231,12 +250,23 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem disposable.dispose(); } emptyView.hide(); - disposable = Observable.fromCallable(DBReader::getNavDrawerData) + disposable = Observable.fromCallable( + () -> { + NavDrawerData data = DBReader.getNavDrawerData(); + List<NavDrawerData.DrawerItem> items = data.items; + for (NavDrawerData.DrawerItem item : items) { + if (item.type == NavDrawerData.DrawerItem.Type.FOLDER + && item.getTitle().equals(displayedFolder)) { + return ((NavDrawerData.FolderDrawerItem) item).children; + } + } + return items; + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( result -> { - navDrawerData = result; + listItems = result; subscriptionAdapter.notifyDataSetChanged(); emptyView.updateVisibility(); progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing @@ -261,40 +291,30 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; - int position = adapterInfo.position; - - Object selectedObject = subscriptionAdapter.getItem(position); - if (selectedObject.equals(SubscriptionsAdapter.ADD_ITEM_OBJ)) { - mPosition = position; + if (menuInfo == null) { return; } + AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo; + int position = adapterInfo.position; - Feed feed = (Feed) selectedObject; - - MenuInflater inflater = requireActivity().getMenuInflater(); - inflater.inflate(R.menu.nav_feed_context, menu); - - menu.setHeaderTitle(feed.getTitle()); + NavDrawerData.DrawerItem selectedObject = (NavDrawerData.DrawerItem) subscriptionAdapter.getItem(position); - mPosition = position; + if (selectedObject.type == NavDrawerData.DrawerItem.Type.FEED) { + MenuInflater inflater = requireActivity().getMenuInflater(); + inflater.inflate(R.menu.nav_feed_context, menu); + selectedFeed = ((NavDrawerData.FeedDrawerItem) selectedObject).feed; + } + menu.setHeaderTitle(selectedObject.getTitle()); } @Override public boolean onContextItemSelected(MenuItem item) { - final int position = mPosition; - mPosition = -1; // reset - if (position < 0) { + if (selectedFeed == null) { return false; } - Object selectedObject = subscriptionAdapter.getItem(position); - if (selectedObject.equals(SubscriptionsAdapter.ADD_ITEM_OBJ)) { - // this is the add object, do nothing - return false; - } - - Feed feed = (Feed) selectedObject; + Feed feed = selectedFeed; + selectedFeed = null; switch (item.getItemId()) { case R.id.remove_all_new_flags_item: displayConfirmationDialog( @@ -359,25 +379,20 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private final SubscriptionsAdapter.ItemAccess itemAccess = new SubscriptionsAdapter.ItemAccess() { @Override public int getCount() { - if (navDrawerData != null) { - return navDrawerData.feeds.size(); + if (listItems != null) { + return listItems.size(); } else { return 0; } } @Override - public Feed getItem(int position) { - if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) { - return navDrawerData.feeds.get(position); + public NavDrawerData.DrawerItem getItem(int position) { + if (listItems != null && 0 <= position && position < listItems.size()) { + return listItems.get(position); } else { return null; } } - - @Override - public int getFeedCounter(long feedId) { - return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0; - } }; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index 7ee0936d0..c813cbf7a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -17,9 +17,9 @@ import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -76,7 +76,8 @@ public abstract class PodcastListFragment extends Fragment { disposable = Observable.fromCallable( () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHosturl()); + GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), + GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); return loadPodcastData(service); }) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java index 64261493d..f51ab032f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java @@ -1,8 +1,8 @@ package de.danoeh.antennapod.fragment.gpodnet; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; import java.util.List; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java index 41b99cdfc..e3cdb8959 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java @@ -4,9 +4,9 @@ import java.util.Collections; import java.util.List; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; /** * Displays suggestions from gpodder.net @@ -17,7 +17,7 @@ public class SuggestionListFragment extends PodcastListFragment { @Override protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { if (GpodnetPreferences.loggedIn()) { - service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + service.login(); return service.getSuggestions(SUGGESTIONS_COUNT); } else { return Collections.emptyList(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java index b60f1bed2..62e2e30d1 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -1,15 +1,14 @@ package de.danoeh.antennapod.fragment.gpodnet; import android.os.Bundle; - -import org.apache.commons.lang3.Validate; +import androidx.annotation.NonNull; import java.util.List; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag; /** * Shows all podcasts from gpodder.net that belong to a specific tag. @@ -22,8 +21,7 @@ public class TagFragment extends PodcastListFragment { private GpodnetTag tag; - public static TagFragment newInstance(GpodnetTag tag) { - Validate.notNull(tag); + public static TagFragment newInstance(@NonNull GpodnetTag tag) { TagFragment fragment = new TagFragment(); Bundle args = new Bundle(); args.putParcelable("tag", tag); @@ -36,7 +34,9 @@ public class TagFragment extends PodcastListFragment { super.onCreate(savedInstanceState); Bundle args = getArguments(); - Validate.isTrue(args != null && args.getParcelable("tag") != null, "args invalid"); + if (args == null || args.getParcelable("tag") == null) { + throw new IllegalArgumentException("Arguments not given"); + } tag = args.getParcelable("tag"); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 9d0f99aa9..f961e30bb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -10,8 +10,8 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -51,7 +51,8 @@ public class TagListFragment extends ListFragment { disposable = Observable.fromCallable( () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHosturl()); + GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), + GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); return service.getTopTags(COUNT); }) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java index 6eb19aff2..c0bf3e0ea 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java @@ -25,8 +25,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; import io.reactivex.Completable; @@ -96,7 +96,9 @@ public class GpodderAuthenticationFragment extends DialogFragment { } else { GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST); } - service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), + GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), + GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); getDialog().setTitle(GpodnetPreferences.getHosturl()); advance(); }); @@ -138,7 +140,8 @@ public class GpodderAuthenticationFragment extends DialogFragment { inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); Completable.fromAction(() -> { - service.authenticate(usernameStr, passwordStr); + service.setCredentials(usernameStr, passwordStr); + service.login(); devices = service.getDevices(); GpodderAuthenticationFragment.this.username = usernameStr; GpodderAuthenticationFragment.this.password = passwordStr; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index 7bf602e35..baf4c7c57 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -1,14 +1,15 @@ package de.danoeh.antennapod.fragment.preferences; import android.content.Intent; -import android.os.Build; import android.os.Bundle; -import android.provider.Settings; + import androidx.appcompat.app.AppCompatActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; + import com.bytehamster.lib.preferencesearch.SearchConfiguration; import com.bytehamster.lib.preferencesearch.SearchPreference; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.BugReportActivity; import de.danoeh.antennapod.activity.PreferenceActivity; @@ -30,6 +31,7 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String STATISTICS = "statistics"; private static final String PREF_ABOUT = "prefAbout"; private static final String PREF_NOTIFICATION = "notifications"; + private static final String PREF_CONTRIBUTE = "prefContribute"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -81,26 +83,21 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { return true; }); findPreference(PREF_NOTIFICATION).setOnPreferenceClickListener(preference -> { - if (Build.VERSION.SDK_INT >= 26) { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName()); - startActivity(intent); - } else { - ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); - } + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_notifications); return true; }); findPreference(PREF_ABOUT).setOnPreferenceClickListener( preference -> { - getParentFragmentManager().beginTransaction().replace(R.id.content, new AboutFragment()) + getParentFragmentManager().beginTransaction() + .replace(R.id.settingsContainer, new AboutFragment()) .addToBackStack(getString(R.string.about_pref)).commit(); return true; } ); findPreference(STATISTICS).setOnPreferenceClickListener( preference -> { - getParentFragmentManager().beginTransaction().replace(R.id.content, new StatisticsFragment()) + getParentFragmentManager().beginTransaction() + .replace(R.id.settingsContainer, new StatisticsFragment()) .addToBackStack(getString(R.string.statistics_label)).commit(); return true; } @@ -113,6 +110,10 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { IntentUtils.openInBrowser(getContext(), "https://forum.antennapod.org/"); return true; }); + findPreference(PREF_CONTRIBUTE).setOnPreferenceClickListener(preference -> { + IntentUtils.openInBrowser(getContext(), "https://antennapod.org/contribute/"); + return true; + }); findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> { startActivity(new Intent(getActivity(), BugReportActivity.class)); return true; @@ -123,7 +124,7 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { SearchPreference searchPreference = findPreference("searchPreference"); SearchConfiguration config = searchPreference.getSearchConfiguration(); config.setActivity((AppCompatActivity) getActivity()); - config.setFragmentContainerViewId(R.id.content); + config.setFragmentContainerViewId(R.id.settingsContainer); config.setBreadcrumbsEnabled(true); config.index(R.xml.preferences_user_interface) @@ -145,5 +146,7 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_gpodder)); config.index(R.xml.preferences_notifications) .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications)); + config.index(R.xml.feed_settings) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.feed_settings)); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java index 3889034fa..305e495e8 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java @@ -1,23 +1,25 @@ package de.danoeh.antennapod.fragment.preferences; -import android.app.TimePickerDialog; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.PreferenceFragmentCompat; import android.text.format.DateFormat; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.dialog.FeedRefreshIntervalDialog; import de.danoeh.antennapod.dialog.ProxyDialog; -import org.apache.commons.lang3.ArrayUtils; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.concurrent.TimeUnit; -public class NetworkPreferencesFragment extends PreferenceFragmentCompat { + +public class NetworkPreferencesFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; private static final String PREF_PROXY = "prefProxy"; @@ -31,6 +33,13 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { public void onStart() { super.onStart(); ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.network_pref); + PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); } @Override @@ -47,9 +56,10 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { }); findPreference(UserPreferences.PREF_UPDATE_INTERVAL) .setOnPreferenceClickListener(preference -> { - showUpdateIntervalTimePreferencesDialog(); + new FeedRefreshIntervalDialog(getContext()).show(); return true; }); + findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) .setOnPreferenceChangeListener( (preference, o) -> { @@ -67,6 +77,9 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { }); } + /** + * Used to init and handle changes to view + */ private void setUpdateIntervalText() { Context context = getActivity().getApplicationContext(); String val; @@ -74,7 +87,7 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { if (interval > 0) { int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); val = context.getResources().getQuantityString( - R.plurals.pref_autoUpdateIntervallOrTime_every_hours, hours, hours); + R.plurals.feed_refresh_every_x_hours, hours, hours); } else { int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); if (timeOfDay.length == 2) { @@ -82,94 +95,29 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); cal.set(Calendar.MINUTE, timeOfDay[1]); String timeOfDayStr = DateFormat.getTimeFormat(context).format(cal.getTime()); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_at), + val = String.format(context.getString(R.string.feed_refresh_interval_at), timeOfDayStr); } else { - val = context.getString(R.string.pref_smart_mark_as_played_disabled); // TODO: Is this a bug? Otherwise document why is this related to smart mark??? + val = context.getString(R.string.feed_refresh_never); } } - String summary = context.getString(R.string.pref_autoUpdateIntervallOrTime_sum) + "\n" + String summary = context.getString(R.string.feed_refresh_sum) + "\n" + String.format(context.getString(R.string.pref_current_value), val); findPreference(UserPreferences.PREF_UPDATE_INTERVAL).setSummary(summary); } private void setParallelDownloadsText(int downloads) { final Resources res = getActivity().getResources(); - String s = res.getString(R.string.parallel_downloads, downloads); findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); } - private void showUpdateIntervalTimePreferencesDialog() { - final Context context = getActivity(); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.pref_autoUpdateIntervallOrTime_title); - builder.setMessage(R.string.pref_autoUpdateIntervallOrTime_message); - builder.setPositiveButton(R.string.pref_autoUpdateIntervallOrTime_Interval, (dialog, which) -> { - AlertDialog.Builder builder1 = new AlertDialog.Builder(context); - builder1.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_Interval)); - final String[] values = context.getResources().getStringArray(R.array.update_intervall_values); - final String[] entries = getUpdateIntervalEntries(values); - long currInterval = UserPreferences.getUpdateInterval(); - int checkedItem = -1; - if(currInterval > 0) { - String currIntervalStr = String.valueOf(TimeUnit.MILLISECONDS.toHours(currInterval)); - checkedItem = ArrayUtils.indexOf(values, currIntervalStr); - } - builder1.setSingleChoiceItems(entries, checkedItem, (dialog1, which1) -> { - int hours = Integer.parseInt(values[which1]); - UserPreferences.setUpdateInterval(hours); - dialog1.dismiss(); - setUpdateIntervalText(); - }); - builder1.setNegativeButton(context.getString(R.string.cancel_label), null); - builder1.show(); - }); - builder.setNegativeButton(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay, (dialog, which) -> { - int hourOfDay = 7; - int minute = 0; - int[] updateTime = UserPreferences.getUpdateTimeOfDay(); - if (updateTime.length == 2) { - hourOfDay = updateTime[0]; - minute = updateTime[1]; - } - TimePickerDialog timePickerDialog = new TimePickerDialog(context, - (view, selectedHourOfDay, selectedMinute) -> { - if (view.getTag() == null) { // onTimeSet() may get called twice! - view.setTag("TAGGED"); - UserPreferences.setUpdateTimeOfDay(selectedHourOfDay, selectedMinute); - setUpdateIntervalText(); - } - }, hourOfDay, minute, DateFormat.is24HourFormat(context)); - timePickerDialog.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay)); - timePickerDialog.show(); - }); - builder.setNeutralButton(R.string.pref_autoUpdateIntervallOrTime_Disable, (dialog, which) -> { - UserPreferences.disableAutoUpdate(context); + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { setUpdateIntervalText(); - }); - builder.show(); - } - - private String[] getUpdateIntervalEntries(final String[] values) { - final Resources res = getActivity().getResources(); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - Integer v = Integer.parseInt(values[x]); - switch (v) { - case 0: - entries[x] = res.getString(R.string.pref_update_interval_hours_manual); - break; - case 1: - entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_singular); - break; - default: - entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_plural); - break; - - } } - return entries; } } + + diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java index 0a64bbe71..e85f60027 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/AboutFragment.java @@ -28,7 +28,8 @@ public class AboutFragment extends PreferenceFragmentCompat { return true; }); findPreference("about_contributors").setOnPreferenceClickListener((preference) -> { - getParentFragmentManager().beginTransaction().replace(R.id.content, new ContributorsPagerFragment()) + getParentFragmentManager().beginTransaction() + .replace(R.id.settingsContainer, new ContributorsPagerFragment()) .addToBackStack(getString(R.string.contributors)).commit(); return true; }); @@ -37,7 +38,8 @@ public class AboutFragment extends PreferenceFragmentCompat { return true; }); findPreference("about_licenses").setOnPreferenceClickListener((preference) -> { - getParentFragmentManager().beginTransaction().replace(R.id.content, new LicensesFragment()) + getParentFragmentManager().beginTransaction() + .replace(R.id.settingsContainer, new LicensesFragment()) .addToBackStack(getString(R.string.translators)).commit(); return true; }); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java index b844234b7..dcd720dc2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/DevelopersFragment.java @@ -29,7 +29,7 @@ public class DevelopersFragment extends ListFragment { developersLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> { ArrayList<SimpleIconListAdapter.ListItem> developers = new ArrayList<>(); BufferedReader reader = new BufferedReader(new InputStreamReader( - getContext().getAssets().open("developers.csv"))); + getContext().getAssets().open("developers.csv"), "UTF-8")); String line; while ((line = reader.readLine()) != null) { String[] info = line.split(";"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/LicensesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/LicensesFragment.java index 97565a613..38e532aed 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/LicensesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/LicensesFragment.java @@ -95,7 +95,7 @@ public class LicensesFragment extends ListFragment { private void showLicenseText(String licenseTextFile) { try { BufferedReader reader = new BufferedReader(new InputStreamReader( - getContext().getAssets().open(licenseTextFile))); + getContext().getAssets().open(licenseTextFile), "UTF-8")); StringBuilder licenseText = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java index d759a5ff2..1b4beeea0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/SpecialThanksFragment.java @@ -29,7 +29,7 @@ public class SpecialThanksFragment extends ListFragment { translatorsLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> { ArrayList<SimpleIconListAdapter.ListItem> translators = new ArrayList<>(); BufferedReader reader = new BufferedReader(new InputStreamReader( - getContext().getAssets().open("special_thanks.csv"))); + getContext().getAssets().open("special_thanks.csv"), "UTF-8")); String line; while ((line = reader.readLine()) != null) { String[] info = line.split(";"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java index b77c74de6..ed0d53145 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/about/TranslatorsFragment.java @@ -29,7 +29,7 @@ public class TranslatorsFragment extends ListFragment { translatorsLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> { ArrayList<SimpleIconListAdapter.ListItem> translators = new ArrayList<>(); BufferedReader reader = new BufferedReader(new InputStreamReader( - getContext().getAssets().open("translators.csv"))); + getContext().getAssets().open("translators.csv"), "UTF-8")); String line; while ((line = reader.readLine()) != null) { String[] info = line.split(";"); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index 1eecccb4c..d478c581d 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -12,15 +12,15 @@ import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.model.EpisodeAction; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; @@ -49,7 +49,7 @@ public class FeedItemMenuHandler { return false; } final boolean hasMedia = selectedItem.getMedia() != null; - final boolean isPlaying = hasMedia && selectedItem.getState() == FeedItem.State.PLAYING; + final boolean isPlaying = hasMedia && FeedItemUtil.isPlaying(selectedItem.getMedia()); final boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE); @@ -249,7 +249,7 @@ public class FeedItemMenuHandler { final Handler h = new Handler(fragment.requireContext().getMainLooper()); final Runnable r = () -> { FeedMedia media = item.getMedia(); - if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) { + if (media != null && FeedItemUtil.hasAlmostEnded(media) && UserPreferences.isAutoDelete()) { DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), media.getId()); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index 0086a75ab..ed0cac05d 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -14,13 +14,13 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.dialog.FilterDialog; import de.danoeh.antennapod.dialog.IntraFeedSortDialog; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index fbfdf537f..b42244160 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -24,10 +24,10 @@ public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuIte final MenuItem queueLock = menu.findItem(R.id.queue_lock); if (UserPreferences.isQueueLocked()) { queueLock.setTitle(de.danoeh.antennapod.R.string.unlock_queue); - queueLock.setIcon(ThemeUtils.getDrawableFromAttr(context, R.attr.ic_lock_open)); + queueLock.setIcon(R.drawable.ic_lock_open); } else { queueLock.setTitle(de.danoeh.antennapod.R.string.lock_queue); - queueLock.setIcon(ThemeUtils.getDrawableFromAttr(context, R.attr.ic_lock_closed)); + queueLock.setIcon(R.drawable.ic_lock_closed); } } diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java index c9bd973cb..5981d3d67 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -11,7 +11,7 @@ import java.util.Arrays; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; diff --git a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java new file mode 100644 index 000000000..ba7acb847 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java @@ -0,0 +1,148 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar { + + private float top; + private float width; + private float center; + private float bottom; + private float density; + private float progressPrimary; + private float progressSecondary; + private float[] dividerPos; + private boolean isHighlighted = false; + private final Paint paintBackground = new Paint(); + private final Paint paintProgressPrimary = new Paint(); + + public ChapterSeekBar(Context context) { + super(context); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setBackground(null); // Removes the thumb shadow + dividerPos = null; + density = context.getResources().getDisplayMetrics().density; + + paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.currently_playing_background)); + paintBackground.setAlpha(128); + paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.colorPrimary)); + } + + /** + * Sets the relative positions of the chapter dividers. + * @param dividerPos of the chapter dividers relative to the duration of the media. + */ + public void setDividerPos(final float[] dividerPos) { + if (dividerPos != null) { + this.dividerPos = new float[dividerPos.length + 2]; + this.dividerPos[0] = 0; + System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length); + this.dividerPos[this.dividerPos.length - 1] = 1; + } else { + this.dividerPos = null; + } + } + + public void highlightCurrentChapter() { + isHighlighted = true; + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + isHighlighted = false; + invalidate(); + } + }, 1000); + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + center = (getBottom() - getPaddingBottom() - getTop() - getPaddingTop()) / 2.0f; + top = center - density * 1.5f; + bottom = center + density * 1.5f; + width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft()); + progressSecondary = getSecondaryProgress() / (float) getMax() * width; + progressPrimary = getProgress() / (float) getMax() * width; + + if (dividerPos == null) { + drawProgress(canvas); + } else { + drawProgressChapters(canvas); + } + drawThumb(canvas); + } + + private void drawProgress(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + canvas.drawRect(0, top, width, bottom, paintBackground); + canvas.drawRect(0, top, progressSecondary, bottom, paintBackground); + canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary); + canvas.restoreToCount(saveCount); + } + + private void drawProgressChapters(Canvas canvas) { + final int saveCount = canvas.save(); + int currChapter = 1; + float chapterMargin = density * 1.2f; + float topExpanded = center - density * 2.0f; + float bottomExpanded = center + density * 2.0f; + + canvas.translate(getPaddingLeft(), getPaddingTop()); + + for (int i = 1; i < dividerPos.length; i++) { + float right = dividerPos[i] * width - chapterMargin; + float left = dividerPos[i - 1] * width; + float rightCurr = dividerPos[currChapter] * width - chapterMargin; + float leftCurr = dividerPos[currChapter - 1] * width; + + canvas.drawRect(left, top, right, bottom, paintBackground); + + if (progressSecondary > 0 && progressSecondary < width) { + if (right < progressSecondary) { + canvas.drawRect(left, top, right, bottom, paintBackground); + } else if (progressSecondary > left) { + canvas.drawRect(left, top, progressSecondary, bottom, paintBackground); + } + } + + if (right < progressPrimary) { + currChapter = i + 1; + canvas.drawRect(left, top, right, bottom, paintProgressPrimary); + } else if (isHighlighted || isPressed()) { + canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground); + canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary); + } else { + canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary); + } + } + canvas.restoreToCount(saveCount); + } + + private void drawThumb(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); + getThumb().draw(canvas); + canvas.restoreToCount(saveCount); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java index eaad11d1a..ce8f08511 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java @@ -2,15 +2,12 @@ package de.danoeh.antennapod.view; import android.content.Context;
import android.database.DataSetObserver;
-import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
-import androidx.annotation.AttrRes;
-import androidx.core.content.ContextCompat;
+import androidx.annotation.DrawableRes;
import androidx.recyclerview.widget.RecyclerView;
-import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -25,7 +22,6 @@ public class EmptyViewHandler { private ListAdapter listAdapter;
private RecyclerView.Adapter<?> recyclerAdapter;
- private final Context context;
private final View emptyView;
private final TextView tvTitle;
private final TextView tvMessage;
@@ -33,7 +29,6 @@ public class EmptyViewHandler { public EmptyViewHandler(Context context) {
emptyView = View.inflate(context, R.layout.empty_view_layout, null);
- this.context = context;
tvTitle = emptyView.findViewById(R.id.emptyViewTitle);
tvMessage = emptyView.findViewById(R.id.emptyViewMessage);
ivIcon = emptyView.findViewById(R.id.emptyViewIcon);
@@ -51,11 +46,8 @@ public class EmptyViewHandler { tvMessage.setText(message);
}
- public void setIcon(@AttrRes int iconAttr) {
- TypedValue typedValue = new TypedValue();
- context.getTheme().resolveAttribute(iconAttr, typedValue, true);
- Drawable d = ContextCompat.getDrawable(context, typedValue.resourceId);
- ivIcon.setImageDrawable(d);
+ public void setIcon(@DrawableRes int icon) {
+ ivIcon.setImageResource(icon);
ivIcon.setVisibility(View.VISIBLE);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java b/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java index 0e1846f1c..ff52df71f 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java +++ b/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java @@ -20,92 +20,181 @@ package de.danoeh.antennapod.view; import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.ViewParent; +import android.view.ViewTreeObserver; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.viewpager2.widget.ViewPager2; +import de.danoeh.antennapod.R; + import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL; import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL; + /** * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. * - * <p>This solution has limitations when using multiple levels of nested scrollable elements + * This solution has limitations when using multiple levels of nested scrollable elements * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). - */ -class NestedScrollableHost extends FrameLayout { + */ // KhaledAlharthi/NestedScrollableHost.java +public class NestedScrollableHost extends FrameLayout { + + private ViewPager2 parentViewPager; + private int touchSlop = 0; + private float initialX = 0f; + private float initialY = 0f; + private int preferVertical = 1; + private int preferHorizontal = 1; + private int scrollDirection = 0; public NestedScrollableHost(@NonNull Context context) { super(context); - touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + init(context); } public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); + init(context); + setAttributes(context, attrs); + } + + public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + setAttributes(context, attrs); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + setAttributes(context, attrs); + } + + private void setAttributes(@NonNull Context context, @Nullable AttributeSet attrs) { + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.NestedScrollableHost, + 0, 0); + + try { + preferHorizontal = a.getInteger(R.styleable.NestedScrollableHost_preferHorizontal, 1); + preferVertical = a.getInteger(R.styleable.NestedScrollableHost_preferVertical, 1); + scrollDirection = a.getInteger(R.styleable.NestedScrollableHost_scrollDirection, 0); + } finally { + a.recycle(); + } + + } + + private void init(Context context) { touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + View v = (View) getParent(); + while (v != null && !(v instanceof ViewPager2) || isntSameDirection(v)) { + v = (View) v.getParent(); + } + parentViewPager = (ViewPager2) v; + + getViewTreeObserver().removeOnPreDrawListener(this); + return false; + } + }); } - private int touchSlop; - private float initialX = 0f; - private float initialY = 0f; + private Boolean isntSameDirection(View v) { + int orientation = 0; + switch (scrollDirection) { + default: + case 0: + return false; + case 1: + orientation = ORIENTATION_VERTICAL; + break; + case 2: + orientation = ORIENTATION_HORIZONTAL; + break; + } + return ((v instanceof ViewPager2) && ((ViewPager2) v).getOrientation() != orientation); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + handleInterceptTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } - private ViewPager2 getParentViewPager() { - View v = (View) getParent(); - while (v != null && !(v instanceof ViewPager2)) { - v = (View) v.getParent(); + + private boolean canChildScroll(int orientation, float delta) { + int direction = (int) -delta; + View child = getChildAt(0); + if (orientation == 0) { + return child.canScrollHorizontally(direction); + } else if (orientation == 1) { + return child.canScrollVertically(direction); + } else { + throw new IllegalArgumentException(); } - return v == null ? null : (ViewPager2) v; } - public boolean onInterceptTouchEvent(MotionEvent e) { - ViewPager2 parentViewPager = getParentViewPager(); + private void handleInterceptTouchEvent(MotionEvent e) { if (parentViewPager == null) { - return super.onInterceptTouchEvent(e); + return; } - - ViewParent parent = getParent(); int orientation = parentViewPager.getOrientation(); + boolean preferedDirection = preferHorizontal + preferVertical > 2; + + // Early return if child can't scroll in same direction as parent and theres no prefered scroll direction + if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f) && !preferedDirection) { + return; + } + if (e.getAction() == MotionEvent.ACTION_DOWN) { initialX = e.getX(); initialY = e.getY(); - parent.requestDisallowInterceptTouchEvent(true); + getParent().requestDisallowInterceptTouchEvent(true); } else if (e.getAction() == MotionEvent.ACTION_MOVE) { - int dx = (int) (e.getX() - initialX); - int dy = (int) (e.getY() - initialY); - boolean isVpHorizontal = orientation == ORIENTATION_HORIZONTAL; + float dx = e.getX() - initialX; + float dy = e.getY() - initialY; + boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL; // assuming ViewPager2 touch-slop is 2x touch-slop of child - float scaledDx = Math.abs(dx) * (isVpHorizontal ? .5f : 1f); - float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1f : .5f); + float scaledDx = Math.abs(dx) * (isVpHorizontal ? 1f : 0.5f) * preferHorizontal; + float scaledDy = Math.abs(dy) * (isVpHorizontal ? 0.5f : 1f) * preferVertical; if (scaledDx > touchSlop || scaledDy > touchSlop) { - int value = isVpHorizontal ? dy : dx; if (isVpHorizontal == (scaledDy > scaledDx)) { - // Gesture is perpendicular - orientation = orientation == ORIENTATION_VERTICAL - ? ORIENTATION_HORIZONTAL : ORIENTATION_VERTICAL; - value = isVpHorizontal ? dy : dx; - } - - int direction = (int) -Math.copySign(1, value); - View child = getChildAt(0); - if (orientation == ORIENTATION_HORIZONTAL) { - parent.requestDisallowInterceptTouchEvent(child.canScrollHorizontally(direction)); + // Gesture is perpendicular, allow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(preferedDirection); } else { - parent.requestDisallowInterceptTouchEvent(child.canScrollVertically(direction)); + // Gesture is parallel, query child if movement in that direction is possible + if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) { + // Child can scroll, disallow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(true); + } else { + // Child cannot scroll, allow all parents to intercept + getParent().requestDisallowInterceptTouchEvent(false); + } } } - } - return super.onInterceptTouchEvent(e); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java b/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java new file mode 100644 index 000000000..04b277fb4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/PlayButton.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageButton; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; +import de.danoeh.antennapod.R; + +public class PlayButton extends AppCompatImageButton { + private boolean isShowPlay = true; + private boolean isVideoScreen = false; + + public PlayButton(@NonNull Context context) { + super(context); + } + + public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public PlayButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setIsVideoScreen(boolean isVideoScreen) { + this.isVideoScreen = isVideoScreen; + } + + public void setIsShowPlay(boolean showPlay) { + if (this.isShowPlay != showPlay) { + this.isShowPlay = showPlay; + setContentDescription(getContext().getString(showPlay ? R.string.play_label : R.string.pause_label)); + if (isVideoScreen) { + setImageResource(showPlay ? R.drawable.ic_play_video_white : R.drawable.ic_pause_video_white); + } else if (!isShown()) { + setImageResource(showPlay ? R.drawable.ic_play_48dp : R.drawable.ic_pause); + } else if (showPlay) { + AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( + getContext(), R.drawable.ic_animate_pause_play); + setImageDrawable(drawable); + drawable.start(); + } else { + AnimatedVectorDrawableCompat drawable = AnimatedVectorDrawableCompat.create( + getContext(), R.drawable.ic_animate_play_pause); + setImageDrawable(drawable); + drawable.start(); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java index 47797e4a4..c75164a74 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java +++ b/app/src/main/java/de/danoeh/antennapod/view/PlaybackSpeedSeekBar.java @@ -10,7 +10,6 @@ import androidx.annotation.Nullable; import androidx.core.util.Consumer; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; public class PlaybackSpeedSeekBar extends FrameLayout { private SeekBar seekBar; @@ -41,7 +40,7 @@ public class PlaybackSpeedSeekBar extends FrameLayout { seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (controller != null && controller.canSetPlaybackSpeed()) { + if (controller != null) { float playbackSpeed = (progress + 10) / 20.0f; controller.setPlaybackSpeed(playbackSpeed); @@ -55,9 +54,6 @@ public class PlaybackSpeedSeekBar extends FrameLayout { @Override public void onStartTrackingTouch(SeekBar seekBar) { - if (controller != null && !controller.canSetPlaybackSpeed()) { - VariableSpeedDialog.showGetPluginDialog(getContext()); - } } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java index 0e446fb84..578e1b149 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadLogItemViewHolder.java @@ -11,25 +11,26 @@ import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.joanzapata.iconify.widget.IconTextView; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.ui.common.CircularProgressBar; -public class DownloadItemViewHolder extends RecyclerView.ViewHolder { +public class DownloadLogItemViewHolder extends RecyclerView.ViewHolder { public final View secondaryActionButton; public final ImageView secondaryActionIcon; + public final CircularProgressBar secondaryActionProgress; public final IconTextView icon; public final TextView title; - public final TextView type; - public final TextView date; + public final TextView status; public final TextView reason; public final TextView tapForDetails; - public DownloadItemViewHolder(Context context, ViewGroup parent) { + public DownloadLogItemViewHolder(Context context, ViewGroup parent) { super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); - date = itemView.findViewById(R.id.txtvDate); - type = itemView.findViewById(R.id.txtvType); + status = itemView.findViewById(R.id.status); icon = itemView.findViewById(R.id.txtvIcon); reason = itemView.findViewById(R.id.txtvReason); tapForDetails = itemView.findViewById(R.id.txtvTapForDetails); secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); title = itemView.findViewById(R.id.txtvTitle); if (Build.VERSION.SDK_INT >= 23) { 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 9fd742d5f..1ea9d71f9 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 @@ -19,9 +19,9 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; @@ -29,6 +29,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.ui.common.CircularProgressBar; @@ -133,7 +134,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); - if (media.isCurrentlyPlaying()) { + if (FeedItemUtil.isCurrentlyPlaying(media)) { itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background)); } else { itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); @@ -152,7 +153,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { duration.setText(Converter.getDurationStringLong(media.getDuration())); duration.setContentDescription(activity.getString(R.string.chapter_duration, Converter.getDurationStringLocalized(activity, media.getDuration()))); - if (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS) { + if (FeedItemUtil.isPlaying(item.getMedia()) || item.isInProgress()) { int progress = (int) (100.0 * media.getPosition() / media.getDuration()); int remainingTime = Math.max(media.getDuration() - media.getPosition(), 0); progressBar.setProgress(progress); @@ -213,7 +214,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } public boolean isCurrentlyPlayingItem() { - return item.getMedia() != null && item.getMedia().isCurrentlyPlaying(); + return item.getMedia() != null && FeedItemUtil.isCurrentlyPlaying(item.getMedia()); } public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { |