diff options
18 files changed, 341 insertions, 174 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 90254c1f3..4aa76f453 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -60,6 +60,9 @@ public class MediaDownloadedHandler implements Runnable { media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)); } + if (media.getItem() != null && media.getItem().getPodcastIndexChapterUrl() != null) { + ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl()); + } // Get duration MediaMetadataRetriever mmr = new MediaMetadataRetriever(); String durationStr = null; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java index 4092087f4..e9f812c7f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import de.danoeh.antennapod.model.feed.Chapter; @@ -11,11 +12,13 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; +import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser; import de.danoeh.antennapod.parser.media.id3.ChapterReader; import de.danoeh.antennapod.parser.media.id3.ID3ReaderException; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader; import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException; +import okhttp3.CacheControl; import okhttp3.Request; import okhttp3.Response; import org.apache.commons.io.input.CountingInputStream; @@ -57,6 +60,7 @@ public class ChapterUtils { } List<Chapter> chaptersFromDatabase = null; + List<Chapter> chaptersFromPodcastIndex = null; if (playable instanceof FeedMedia) { FeedMedia feedMedia = (FeedMedia) playable; if (feedMedia.getItem() == null) { @@ -65,10 +69,17 @@ public class ChapterUtils { if (feedMedia.getItem().hasChapters()) { chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem()); } + + if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) { + chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl( + feedMedia.getItem().getPodcastIndexChapterUrl()); + } + } List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context); - List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); + List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); + List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex); if (chapters == null) { // Do not try loading again. There are no chapters. playable.setChapters(Collections.emptyList()); @@ -123,6 +134,27 @@ public class ChapterUtils { } } + public static List<Chapter> loadChaptersFromUrl(String url) { + try { + Request request = new Request.Builder().url(url).cacheControl(CacheControl.FORCE_CACHE).build(); + Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute(); + if (response.isSuccessful() && response.body() != null) { + List<Chapter> chapters = PodcastIndexChapterParser.parse(response.body().string()); + if (chapters != null && !chapters.isEmpty()) { + return chapters; + } + } + request = new Request.Builder().url(url).build(); + response = AntennapodHttpClient.getHttpClient().newCall(request).execute(); + if (response.isSuccessful() && response.body() != null) { + return PodcastIndexChapterParser.parse(response.body().string()); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + @NonNull private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException { ChapterReader reader = new ChapterReader(in); diff --git a/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java b/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java new file mode 100644 index 000000000..d01e6a059 --- /dev/null +++ b/event/src/main/java/de/danoeh/antennapod/event/StatisticsEvent.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.event; + +public class StatisticsEvent { + + public StatisticsEvent() { + } +} diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java index 08f79252a..a8570ea4e 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java @@ -41,6 +41,7 @@ public class FeedItem extends FeedComponent implements Serializable { private transient Feed feed; private long feedId; + private String podcastIndexChapterUrl; private int state; public static final int NEW = -1; @@ -81,7 +82,7 @@ public class FeedItem extends FeedComponent implements Serializable { * */ public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId, boolean hasChapters, String imageUrl, int state, - String itemIdentifier, long autoDownload) { + String itemIdentifier, long autoDownload, String podcastIndexChapterUrl) { this.id = id; this.title = title; this.link = link; @@ -93,6 +94,7 @@ public class FeedItem extends FeedComponent implements Serializable { this.state = state; this.itemIdentifier = itemIdentifier; this.autoDownload = autoDownload; + this.podcastIndexChapterUrl = podcastIndexChapterUrl; } /** @@ -157,6 +159,9 @@ public class FeedItem extends FeedComponent implements Serializable { chapters = other.chapters; } } + if (other.podcastIndexChapterUrl != null) { + podcastIndexChapterUrl = other.podcastIndexChapterUrl; + } } /** @@ -427,6 +432,14 @@ public class FeedItem extends FeedComponent implements Serializable { tags.remove(tag); } + public String getPodcastIndexChapterUrl() { + return podcastIndexChapterUrl; + } + + public void setPodcastIndexChapterUrl(String url) { + podcastIndexChapterUrl = url; + } + @NonNull @Override public String toString() { diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java new file mode 100644 index 000000000..5dcc18b14 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/PodcastIndexChapterParser.java @@ -0,0 +1,31 @@ +package de.danoeh.antennapod.parser.feed; + +import de.danoeh.antennapod.model.feed.Chapter; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class PodcastIndexChapterParser { + public static List<Chapter> parse(String jsonStr) { + try { + List<Chapter> chapters = new ArrayList<>(); + JSONObject obj = new JSONObject(jsonStr); + JSONArray objChapters = obj.getJSONArray("chapters"); + for (int i = 0; i < objChapters.length(); i++) { + JSONObject jsonObject = objChapters.getJSONObject(i); + int startTime = jsonObject.optInt("startTime", 0); + String title = jsonObject.optString("title"); + String link = jsonObject.optString("url"); + String img = jsonObject.optString("img"); + chapters.add(new Chapter(startTime * 1000L, title, link, img)); + } + return chapters; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java index 1d4a91192..1f543a5ae 100644 --- a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java @@ -1,8 +1,8 @@ package de.danoeh.antennapod.parser.feed.namespace; +import android.text.TextUtils; import de.danoeh.antennapod.parser.feed.HandlerState; import de.danoeh.antennapod.parser.feed.element.SyndElement; -import org.jsoup.helper.StringUtil; import org.xml.sax.Attributes; import de.danoeh.antennapod.model.feed.FeedFunding; @@ -13,6 +13,7 @@ public class PodcastIndex extends Namespace { public static final String NSURI2 = "https://podcastindex.org/namespace/1.0"; private static final String URL = "url"; private static final String FUNDING = "funding"; + private static final String CHAPTERS = "chapters"; @Override public SyndElement handleElementStart(String localName, HandlerState state, @@ -22,6 +23,11 @@ public class PodcastIndex extends Namespace { FeedFunding funding = new FeedFunding(href, ""); state.setCurrentFunding(funding); state.getFeed().addPayment(state.getCurrentFunding()); + } else if (CHAPTERS.equals(localName)) { + String href = attributes.getValue(URL); + if (!TextUtils.isEmpty(href)) { + state.getCurrentItem().setPodcastIndexChapterUrl(href); + } } return new SyndElement(localName, this); } @@ -32,7 +38,7 @@ public class PodcastIndex extends Namespace { return; } String content = state.getContentBuf().toString(); - if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !StringUtil.isBlank(content)) { + if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !TextUtils.isEmpty(content)) { state.getCurrentFunding().setContent(content); } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java index 1954a5652..78eaf6964 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBUpgrader.java @@ -326,6 +326,10 @@ class DBUpgrader { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1"); } + if (oldVersion < 2060000) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL + " TEXT"); + } } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java index 7994861e8..f66c385b7 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java @@ -50,7 +50,7 @@ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; public static final String DATABASE_NAME = "Antennapod.db"; - public static final int VERSION = 2050000; + public static final int VERSION = 2060000; /** * Maximum number of arguments for IN-operator. @@ -116,6 +116,7 @@ public class PodDBAdapter { public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; public static final String KEY_FEED_TAGS = "tags"; public static final String KEY_EPISODE_NOTIFICATION = "episode_notification"; + public static final String KEY_PODCASTINDEX_CHAPTER_URL = "podcastindex_chapter_url"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -166,7 +167,8 @@ public class PodDBAdapter { + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + KEY_IMAGE_URL + " TEXT," - + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)"; + + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER," + + KEY_PODCASTINDEX_CHAPTER_URL + " TEXT)"; private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION @@ -292,7 +294,8 @@ public class PodDBAdapter { + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", " - + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS; + + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS + ", " + + TABLE_NAME_FEED_ITEMS + "." + KEY_PODCASTINDEX_CHAPTER_URL; private static final String KEYS_FEED_MEDIA = TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", " @@ -648,6 +651,7 @@ public class PodDBAdapter { values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime()); values.put(KEY_IMAGE_URL, item.getImageUrl()); + values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl()); if (item.getId() == 0) { item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java index 799ca5dde..fcf51e31e 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/FeedItemCursorMapper.java @@ -27,6 +27,7 @@ public abstract class FeedItemCursorMapper { int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER); int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS); int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL); + int indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL); long id = cursor.getInt(indexId); String title = cursor.getString(indexTitle); @@ -39,8 +40,9 @@ public abstract class FeedItemCursorMapper { String itemIdentifier = cursor.getString(indexItemIdentifier); long autoDownload = cursor.getLong(indexAutoDownload); String imageUrl = cursor.getString(indexImageUrl); + String podcastIndexChapterUrl = cursor.getString(indexPodcastIndexChapterUrl); return new FeedItem(id, title, link, pubDate, paymentLink, feedId, - hasChapters, imageUrl, state, itemIdentifier, autoDownload); + hasChapters, imageUrl, state, itemIdentifier, autoDownload, podcastIndexChapterUrl); } } diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java index 118d2ffb2..cbdd789db 100644 --- a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PagedToolbarFragment.java @@ -29,6 +29,9 @@ public abstract class PagedToolbarFragment extends Fragment { this.viewPager = viewPager; toolbar.setOnMenuItemClickListener(item -> { + if (this.onOptionsItemSelected(item)) { + return true; + } Fragment child = getChildFragmentManager().findFragmentByTag("f" + viewPager.getCurrentItem()); if (child != null) { return child.onOptionsItemSelected(item); diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index a75301df1..b29728d16 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -49,7 +49,7 @@ <string name="statistics_to">To</string> <string name="statistics_today">Today</string> <string name="statistics_filter_all_time">All time</string> - <string name="statistics_filter_last_year">Last year</string> + <string name="statistics_filter_past_year">Past year</string> <string name="statistics_reset_data">Reset statistics data</string> <string name="statistics_reset_data_msg">This will erase the history of duration played for all episodes. Are you sure you want to proceed?</string> <string name="statistics_counting_range">Played between %1$s and %2$s</string> diff --git a/ui/statistics/build.gradle b/ui/statistics/build.gradle index 49ec1df8b..cca840989 100644 --- a/ui/statistics/build.gradle +++ b/ui/statistics/build.gradle @@ -13,6 +13,7 @@ android { dependencies { implementation project(":core") + implementation project(':event') implementation project(":model") implementation project(":ui:common") @@ -24,6 +25,7 @@ dependencies { implementation "androidx.viewpager2:viewpager2:$viewPager2Version" implementation "com.google.android.material:material:$googleMaterialVersion" + implementation "org.greenrobot:eventbus:$eventbusVersion" implementation "com.github.bumptech.glide:glide:$glideVersion" annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java index fc0f590cd..53a45f248 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java @@ -1,9 +1,13 @@ package de.danoeh.antennapod.ui.statistics; +import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.util.Log; import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; @@ -13,17 +17,29 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.event.StatisticsEvent; import de.danoeh.antennapod.ui.common.PagedToolbarFragment; import de.danoeh.antennapod.ui.statistics.downloads.DownloadStatisticsFragment; import de.danoeh.antennapod.ui.statistics.subscriptions.SubscriptionStatisticsFragment; import de.danoeh.antennapod.ui.statistics.years.YearsStatisticsFragment; +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; /** * Displays the 'statistics' screen */ public class StatisticsFragment extends PagedToolbarFragment { - public static final String TAG = "StatisticsFragment"; + public static final String PREF_NAME = "StatisticsActivityPrefs"; + public static final String PREF_INCLUDE_MARKED_PLAYED = "countAll"; + public static final String PREF_FILTER_FROM = "filterFrom"; + public static final String PREF_FILTER_TO = "filterTo"; + private static final int POS_SUBSCRIPTIONS = 0; private static final int POS_YEARS = 1; @@ -68,6 +84,44 @@ public class StatisticsFragment extends PagedToolbarFragment { return rootView; } + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.statistics_reset) { + confirmResetStatistics(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void confirmResetStatistics() { + ConfirmationDialog conDialog = new ConfirmationDialog( + getActivity(), + R.string.statistics_reset_data, + R.string.statistics_reset_data_msg) { + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + dialog.dismiss(); + doResetStatistics(); + } + }; + conDialog.createNewDialog().show(); + } + + private void doResetStatistics() { + getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() + .putBoolean(PREF_INCLUDE_MARKED_PLAYED, false) + .putLong(PREF_FILTER_FROM, 0) + .putLong(PREF_FILTER_TO, Long.MAX_VALUE) + .apply(); + + Disposable disposable = Completable.fromFuture(DBWriter.resetStatistics()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> EventBus.getDefault().post(new StatisticsEvent()), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + public static class StatisticsPagerAdapter extends FragmentStateAdapter { StatisticsPagerAdapter(@NonNull Fragment fragment) { diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java index 662f96775..3936118ca 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/PlaybackStatisticsListAdapter.java @@ -41,7 +41,8 @@ public class PlaybackStatisticsListAdapter extends StatisticsListAdapter { } SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy", Locale.getDefault()); String dateFrom = dateFormat.format(new Date(timeFilterFrom)); - String dateTo = dateFormat.format(new Date(timeFilterTo)); + // FilterTo is first day of next month => Subtract one day + String dateTo = dateFormat.format(new Date(timeFilterTo - 24L * 3600000L)); return context.getString(R.string.statistics_counting_range, dateFrom, dateTo); } diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java new file mode 100644 index 000000000..93f8e7715 --- /dev/null +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/StatisticsFilterDialog.java @@ -0,0 +1,135 @@ +package de.danoeh.antennapod.ui.statistics.subscriptions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.widget.ArrayAdapter; +import androidx.appcompat.app.AlertDialog; +import androidx.core.util.Pair; +import de.danoeh.antennapod.event.StatisticsEvent; +import de.danoeh.antennapod.ui.statistics.R; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; +import de.danoeh.antennapod.ui.statistics.databinding.StatisticsFilterDialogBinding; +import org.greenrobot.eventbus.EventBus; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public class StatisticsFilterDialog { + private final Context context; + private final SharedPreferences prefs; + private boolean includeMarkedAsPlayed; + private long timeFilterFrom; + private long timeFilterTo; + private final Pair<String[], Long[]> filterDatesFrom; + private final Pair<String[], Long[]> filterDatesTo; + + public StatisticsFilterDialog(Context context, long oldestDate) { + this.context = context; + prefs = context.getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE); + includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false); + timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0); + timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE); + filterDatesFrom = makeMonthlyList(oldestDate, false); + filterDatesTo = makeMonthlyList(oldestDate, true); + } + + public void show() { + StatisticsFilterDialogBinding dialogBinding = StatisticsFilterDialogBinding.inflate( + LayoutInflater.from(context)); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setView(dialogBinding.getRoot()); + builder.setTitle(R.string.filter); + dialogBinding.includeMarkedCheckbox.setOnCheckedChangeListener((compoundButton, checked) -> { + dialogBinding.timeToSpinner.setEnabled(!checked); + dialogBinding.timeFromSpinner.setEnabled(!checked); + dialogBinding.pastYearButton.setEnabled(!checked); + dialogBinding.allTimeButton.setEnabled(!checked); + dialogBinding.dateSelectionContainer.setAlpha(checked ? 0.5f : 1f); + }); + dialogBinding.includeMarkedCheckbox.setChecked(includeMarkedAsPlayed); + + + ArrayAdapter<String> adapterFrom = new ArrayAdapter<>(context, + android.R.layout.simple_spinner_item, filterDatesFrom.first); + adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + dialogBinding.timeFromSpinner.setAdapter(adapterFrom); + for (int i = 0; i < filterDatesFrom.second.length; i++) { + if (filterDatesFrom.second[i] >= timeFilterFrom) { + dialogBinding.timeFromSpinner.setSelection(i); + break; + } + } + + ArrayAdapter<String> adapterTo = new ArrayAdapter<>(context, + android.R.layout.simple_spinner_item, filterDatesTo.first); + adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + dialogBinding.timeToSpinner.setAdapter(adapterTo); + for (int i = 0; i < filterDatesTo.second.length; i++) { + if (filterDatesTo.second[i] >= timeFilterTo) { + dialogBinding.timeToSpinner.setSelection(i); + break; + } + } + + dialogBinding.allTimeButton.setOnClickListener(v -> { + dialogBinding.timeFromSpinner.setSelection(0); + dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.length - 1); + }); + dialogBinding.pastYearButton.setOnClickListener(v -> { + dialogBinding.timeFromSpinner.setSelection(Math.max(0, filterDatesFrom.first.length - 13)); + dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.length - 2); + }); + + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + includeMarkedAsPlayed = dialogBinding.includeMarkedCheckbox.isChecked(); + if (includeMarkedAsPlayed) { + // We do not know the date at which something was marked as played, so filtering does not make sense + timeFilterFrom = 0; + timeFilterTo = Long.MAX_VALUE; + } else { + timeFilterFrom = filterDatesFrom.second[dialogBinding.timeFromSpinner.getSelectedItemPosition()]; + timeFilterTo = filterDatesTo.second[dialogBinding.timeToSpinner.getSelectedItemPosition()]; + } + prefs.edit() + .putBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed) + .putLong(StatisticsFragment.PREF_FILTER_FROM, timeFilterFrom) + .putLong(StatisticsFragment.PREF_FILTER_TO, timeFilterTo) + .apply(); + EventBus.getDefault().post(new StatisticsEvent()); + }); + builder.show(); + } + + private Pair<String[], Long[]> makeMonthlyList(long oldestDate, boolean inclusive) { + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(oldestDate); + date.set(Calendar.DAY_OF_MONTH, 1); + ArrayList<String> names = new ArrayList<>(); + ArrayList<Long> timestamps = new ArrayList<>(); + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy", Locale.getDefault()); + while (date.getTimeInMillis() < System.currentTimeMillis()) { + names.add(dateFormat.format(new Date(date.getTimeInMillis()))); + if (!inclusive) { + timestamps.add(date.getTimeInMillis()); + } + if (date.get(Calendar.MONTH) == Calendar.DECEMBER) { + date.set(Calendar.MONTH, Calendar.JANUARY); + date.set(Calendar.YEAR, date.get(Calendar.YEAR) + 1); + } else { + date.set(Calendar.MONTH, date.get(Calendar.MONTH) + 1); + } + if (inclusive) { + timestamps.add(date.getTimeInMillis()); + } + } + if (inclusive) { + names.add(context.getString(R.string.statistics_today)); + timestamps.add(Long.MAX_VALUE); + } + return new Pair<>(names.toArray(new String[0]), timestamps.toArray(new Long[0])); + } +} diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java index 79cacfa82..c7eddd222 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/subscriptions/SubscriptionStatisticsFragment.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.ui.statistics.subscriptions; import android.content.Context; -import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; @@ -10,62 +9,41 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ProgressBar; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; -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 de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.event.StatisticsEvent; import de.danoeh.antennapod.ui.statistics.R; -import de.danoeh.antennapod.ui.statistics.databinding.StatisticsFilterDialogBinding; -import io.reactivex.Completable; +import de.danoeh.antennapod.ui.statistics.StatisticsFragment; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; -import java.util.Locale; /** * Displays the 'playback statistics' screen */ public class SubscriptionStatisticsFragment extends Fragment { private static final String TAG = SubscriptionStatisticsFragment.class.getSimpleName(); - private static final String PREF_NAME = "StatisticsActivityPrefs"; - private static final String PREF_INCLUDE_MARKED_PLAYED = "countAll"; - private static final String PREF_FILTER_FROM = "filterFrom"; - private static final String PREF_FILTER_TO = "filterTo"; private Disposable disposable; private RecyclerView feedStatisticsList; private ProgressBar progressBar; private PlaybackStatisticsListAdapter listAdapter; - private boolean includeMarkedAsPlayed = false; - private long timeFilterFrom = 0; - private long timeFilterTo = Long.MAX_VALUE; - private SharedPreferences prefs; private DBReader.StatisticsResult statisticsResult; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - prefs = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - includeMarkedAsPlayed = prefs.getBoolean(PREF_INCLUDE_MARKED_PLAYED, false); - timeFilterFrom = prefs.getLong(PREF_FILTER_FROM, 0); - timeFilterTo = prefs.getLong(PREF_FILTER_TO, Long.MAX_VALUE); setHasOptionsMenu(true); } @@ -79,6 +57,7 @@ public class SubscriptionStatisticsFragment extends Fragment { listAdapter = new PlaybackStatisticsListAdapter(this); feedStatisticsList.setLayoutManager(new LinearLayoutManager(getContext())); feedStatisticsList.setAdapter(listAdapter); + EventBus.getDefault().register(this); return root; } @@ -91,11 +70,17 @@ public class SubscriptionStatisticsFragment extends Fragment { @Override public void onDestroyView() { super.onDestroyView(); + EventBus.getDefault().unregister(this); if (disposable != null) { disposable.dispose(); } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void statisticsEvent(StatisticsEvent event) { + refreshStatistics(); + } + @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); @@ -106,144 +91,14 @@ public class SubscriptionStatisticsFragment extends Fragment { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.statistics_filter) { - selectStatisticsFilter(); - return true; - } else if (item.getItemId() == R.id.statistics_reset) { - confirmResetStatistics(); + if (statisticsResult != null) { + new StatisticsFilterDialog(getContext(), statisticsResult.oldestDate).show(); + } return true; } return super.onOptionsItemSelected(item); } - private void selectStatisticsFilter() { - if (statisticsResult == null) { - return; - } - StatisticsFilterDialogBinding dialogBinding = StatisticsFilterDialogBinding.inflate(getLayoutInflater()); - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setView(dialogBinding.getRoot()); - builder.setTitle(R.string.filter); - dialogBinding.includeMarkedCheckbox.setOnCheckedChangeListener((compoundButton, checked) -> { - dialogBinding.timeToSpinner.setEnabled(!checked); - dialogBinding.timeFromSpinner.setEnabled(!checked); - dialogBinding.lastYearButton.setEnabled(!checked); - dialogBinding.allTimeButton.setEnabled(!checked); - dialogBinding.dateSelectionContainer.setAlpha(checked ? 0.5f : 1f); - }); - dialogBinding.includeMarkedCheckbox.setChecked(includeMarkedAsPlayed); - - Pair<String[], Long[]> filterDates = makeMonthlyList(statisticsResult.oldestDate); - - ArrayAdapter<String> adapterFrom = new ArrayAdapter<>(getContext(), - android.R.layout.simple_spinner_item, filterDates.first); - adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - dialogBinding.timeFromSpinner.setAdapter(adapterFrom); - for (int i = 0; i < filterDates.second.length; i++) { - if (filterDates.second[i] >= timeFilterFrom) { - dialogBinding.timeFromSpinner.setSelection(i); - break; - } - } - - ArrayAdapter<String> adapterTo = new ArrayAdapter<>(getContext(), - android.R.layout.simple_spinner_item, filterDates.first); - adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - dialogBinding.timeToSpinner.setAdapter(adapterTo); - for (int i = 0; i < filterDates.second.length; i++) { - if (filterDates.second[i] >= timeFilterTo) { - dialogBinding.timeToSpinner.setSelection(i); - break; - } - } - - dialogBinding.allTimeButton.setOnClickListener(v -> { - dialogBinding.timeFromSpinner.setSelection(0); - dialogBinding.timeToSpinner.setSelection(filterDates.first.length - 1); - }); - dialogBinding.lastYearButton.setOnClickListener(v -> { - dialogBinding.timeFromSpinner.setSelection(Math.max(0, filterDates.first.length - 14)); - dialogBinding.timeToSpinner.setSelection(filterDates.first.length - 2); - }); - - builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { - includeMarkedAsPlayed = dialogBinding.includeMarkedCheckbox.isChecked(); - if (includeMarkedAsPlayed) { - // We do not know the date at which something was marked as played, so filtering does not make sense - timeFilterFrom = 0; - timeFilterTo = Long.MAX_VALUE; - } else { - timeFilterFrom = filterDates.second[dialogBinding.timeFromSpinner.getSelectedItemPosition()]; - timeFilterTo = filterDates.second[dialogBinding.timeToSpinner.getSelectedItemPosition()]; - } - prefs.edit() - .putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed) - .putLong(PREF_FILTER_FROM, timeFilterFrom) - .putLong(PREF_FILTER_TO, timeFilterTo) - .apply(); - refreshStatistics(); - }); - builder.show(); - } - - private Pair<String[], Long[]> makeMonthlyList(long oldestDate) { - Calendar date = Calendar.getInstance(); - date.setTimeInMillis(oldestDate); - date.set(Calendar.DAY_OF_MONTH, 1); - ArrayList<String> names = new ArrayList<>(); - ArrayList<Long> timestamps = new ArrayList<>(); - SimpleDateFormat dateFormat = new SimpleDateFormat("MMM yyyy", Locale.getDefault()); - while (date.getTimeInMillis() < System.currentTimeMillis()) { - names.add(dateFormat.format(new Date(date.getTimeInMillis()))); - timestamps.add(date.getTimeInMillis()); - if (date.get(Calendar.MONTH) == Calendar.DECEMBER) { - date.set(Calendar.MONTH, Calendar.JANUARY); - date.set(Calendar.YEAR, date.get(Calendar.YEAR) + 1); - } else { - date.set(Calendar.MONTH, date.get(Calendar.MONTH) + 1); - } - } - names.add(getString(R.string.statistics_today)); - timestamps.add(Long.MAX_VALUE); - return new Pair<>(names.toArray(new String[0]), timestamps.toArray(new Long[0])); - } - - private void confirmResetStatistics() { - ConfirmationDialog conDialog = new ConfirmationDialog( - getActivity(), - R.string.statistics_reset_data, - R.string.statistics_reset_data_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - doResetStatistics(); - } - }; - conDialog.createNewDialog().show(); - } - - private void doResetStatistics() { - progressBar.setVisibility(View.VISIBLE); - feedStatisticsList.setVisibility(View.GONE); - if (disposable != null) { - disposable.dispose(); - } - - includeMarkedAsPlayed = false; - timeFilterFrom = 0; - timeFilterTo = Long.MAX_VALUE; - prefs.edit() - .putBoolean(PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed) - .putLong(PREF_FILTER_FROM, timeFilterFrom) - .putLong(PREF_FILTER_TO, timeFilterTo) - .apply(); - - disposable = Completable.fromFuture(DBWriter.resetStatistics()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::refreshStatistics, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - private void refreshStatistics() { progressBar.setVisibility(View.VISIBLE); feedStatisticsList.setVisibility(View.GONE); @@ -254,6 +109,10 @@ public class SubscriptionStatisticsFragment extends Fragment { if (disposable != null) { disposable.dispose(); } + SharedPreferences prefs = getContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE); + boolean includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false); + long timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0); + long timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE); disposable = Observable.fromCallable( () -> { DBReader.StatisticsResult statisticsData = DBReader.getStatistics( diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java index 89a2689ae..5368e52a9 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearsStatisticsFragment.java @@ -13,11 +13,15 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.event.StatisticsEvent; import de.danoeh.antennapod.ui.statistics.R; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Displays the yearly statistics screen @@ -40,6 +44,7 @@ public class YearsStatisticsFragment extends Fragment { listAdapter = new YearStatisticsListAdapter(getContext()); yearStatisticsList.setLayoutManager(new LinearLayoutManager(getContext())); yearStatisticsList.setAdapter(listAdapter); + EventBus.getDefault().register(this); return root; } @@ -52,15 +57,21 @@ public class YearsStatisticsFragment extends Fragment { @Override public void onDestroyView() { super.onDestroyView(); + EventBus.getDefault().unregister(this); if (disposable != null) { disposable.dispose(); } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void statisticsEvent(StatisticsEvent event) { + refreshStatistics(); + } + @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.statistics_reset).setVisible(false); + menu.findItem(R.id.statistics_reset).setVisible(true); menu.findItem(R.id.statistics_filter).setVisible(false); } diff --git a/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml b/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml index d37226c07..684ceabd2 100644 --- a/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml +++ b/ui/statistics/src/main/res/layout/statistics_filter_dialog.xml @@ -65,10 +65,10 @@ android:orientation="horizontal"> <Button - android:id="@+id/lastYearButton" + android:id="@+id/past_year_button" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/statistics_filter_last_year" + android:text="@string/statistics_filter_past_year" android:layout_weight="1" android:layout_marginEnd="4dp" style="@style/Widget.MaterialComponents.Button.OutlinedButton" /> |