Compare commits

...

615 Commits

Author SHA1 Message Date
André 38f56d6d9b
Added option to switch the app language (via android system settings) (#7214) 2024-05-31 21:05:38 +02:00
André 43d487f7e9
Name of the sync server in error messages (#7213) 2024-05-31 10:51:58 +02:00
André f8c864f553
Reorganize appearance settings (#7207) 2024-05-31 10:46:59 +02:00
ByteHamster 0a768e6286 Merge branch 'master' into develop 2024-05-29 18:27:53 +02:00
ByteHamster 1fc212ee88
Move auto-delete settings (#7096)
Users had a hard time understanding that automatic deletion and episode cleanup are two different things.
Maybe that is because in German, both got translated to the exact same string.
Now both are next to each other and the titles are updated, so that it hopefully causes less confusion.
2024-05-24 14:21:21 +02:00
ByteHamster 155d769fca
Fix marking as played when there is no media (#7192) 2024-05-24 10:00:28 +02:00
ByteHamster baeb0d8ced
Fix UrlChecker on antennapod_local urls (#7191) 2024-05-22 10:03:59 +02:00
ByteHamster fc1c13f4a5
Add new date format (#7189) 2024-05-20 20:25:12 +02:00
hades 84b6f442fc
Fix android auto resume on reconnect issues (#7156)
Previously the MediaSession object created in PlaybackService in onCreate would
be completely empty. This seemed to confuse Android Auto, and prevented it from
restarting playback.

Filling the MediaSession object using the data from the player state at
onCreate resolves this problem.

This is documented in Android Auto docs[1], albeit indirectly and somewhat
confusingly.

Also move the setSessionToken call to the end of onCreate handler to ensure
that the media session has already been completely filled by the time the
session token is made available to the framework. There is no evidence that
this is required; however intuitively, this is likely the trigger for the
framework to start querying the media session.

The change was tested both with Desktop Head Unit and with a real vehicle.

[1] https://developer.android.com/training/cars/media/#initial-playback-state
2024-05-18 19:34:36 +02:00
ByteHamster dd8bf381c4
Merge pull request #7186 from AntennaPod/transcript
Podcast:Transcript support
2024-05-18 19:26:39 +02:00
Tony Tam e856a9f118 Display transcript text and follow along the audio (#7103) 2024-05-18 18:58:36 +02:00
Tony Tam 7c4f19c979 Transcript semantic parsing (#6852) 2024-05-18 18:58:01 +02:00
Tony Tam 27e9bf36b1 Download and store transcript text (#6797) 2024-05-18 18:58:01 +02:00
Tony Tam 8adbad9b66 Parse podcast:transcript url and store in SQLite (#6739) 2024-05-18 18:57:57 +02:00
ByteHamster 5f5d744e71 Bump version to 3.4.0 2024-05-12 21:22:49 +02:00
ByteHamster aa23656770 Update metadata repo 2024-05-12 21:21:10 +02:00
ByteHamster 568c0928c5 Update translations 2024-05-12 21:17:37 +02:00
ByteHamster 59c5042a65
Make it possible to scroll swipe actions dialog (#7174) 2024-05-10 08:14:03 +02:00
ByteHamster 8d3eb6aae9
NoRelayoutTextView: Enable requestLayout in more cases (#7175) 2024-05-10 08:11:28 +02:00
ByteHamster 084723ad76
Add episodes without subscribing (#7098) 2024-05-09 11:44:26 +02:00
ByteHamster 53ce6cd71a
Update error message design (#7167) 2024-05-08 23:27:04 +02:00
ByteHamster a61f548792
Fix settings toolbar having color (#7169) 2024-05-08 07:46:25 +02:00
flofriday 2827f41430
Improve layout for missing chapter images (#7164)
If only some chapters have images the other chapters don't display
anything but reserve space for the image.

Now those chapters display the image of the episode. If no chapters have
images no images will be displayed (just like before).
2024-05-06 22:14:26 +02:00
flofriday 6f572faa77
Fix inconsistent icons in the app toolbar. (#7163) 2024-05-06 22:04:24 +02:00
Simon Conrad ba14510b80
Add support for parsing Nero M4A chapters (#7159) 2024-05-05 10:05:26 +02:00
ByteHamster 87bfe1ea8c Bump version to 3.4.0-beta5 2024-05-03 22:20:13 +02:00
ByteHamster cb1a03cd8d
Show statistics above description on feed info page (#7161) 2024-05-03 21:42:14 +02:00
ByteHamster 19396c1e17
Fix password protected feeds (#7155) 2024-05-01 11:52:46 +02:00
hades 292a21f8f8
Playback: remove special handling for Huawei (#7152)
This was introduced in 5105cdd7 to prevent crashes on Huawei phones[1].
However, it seems this has already regressed in 376ffed5, where the setActive
call was moved outside of the try-catch block.

Additionally, the problem is now 9 years old, and hopefully fixed for the users
already.

[1] https://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat
2024-05-01 11:36:15 +02:00
flofriday 3ed5b8af8c
Fix deleting downloaded episode removes from queue (#7151) 2024-05-01 11:33:48 +02:00
ByteHamster a8dfe6f123
Bottom multi-select (#7093) 2024-04-29 07:40:03 +02:00
ByteHamster b877344a7e Bump version to 3.4.0-beta4 2024-04-29 00:01:45 +02:00
ByteHamster 1505c50b1b
Fix sometimes not resetting media position (#7147)
Before 5218e06904, deleting an item
loaded its state from the database again. Now it stores the state
of that object. markItemPlayed() did not reset the object's playback
position, so when auto-delete was enabled, the position was overwritten again.
2024-04-28 23:56:23 +02:00
ByteHamster 257c3bca5e
Fix tests creating FeedItems just once because of duplicate IDs (#7148) 2024-04-28 22:11:37 +02:00
ByteHamster 35817876bf Bump version to 3.4.0-beta3 2024-04-27 14:14:52 +02:00
ByteHamster 0341accef5 Update translations 2024-04-27 14:04:36 +02:00
flofriday c063c59af3
Fix sharp corners on placeholders (#7142)
All placeholder now have round corners matching the corner radius of the
image that will eventually load.
2024-04-27 11:28:30 +02:00
0x082c8bf1 f69822582d
Use multiple threads for refreshing feeds (#7126) 2024-04-27 10:44:09 +02:00
ByteHamster d9d48674ed
Move 'show subscription title' setting to subscription page (#7097) 2024-04-27 10:42:54 +02:00
ByteHamster 4d79419e8e
Switch Emulator CI to Ubuntu (#7143)
GitHub switched their MacOS runners to ARM, which makes the Android emulator fail to start. Since we introduced the CI workflow, GitHub upgraded the Ubuntu runners as well, now supporting hardware acceleration. This means we no longer need MacOS. The Ubuntu runner is also about 2 times faster.
2024-04-27 10:37:32 +02:00
ByteHamster dbbb21bd3b
Switch Emulator CI to Ubuntu (#7140)
GitHub switched their MacOS runners to ARM, which makes the Android emulator fail to start. Since we introduced the CI workflow, GitHub upgraded the Ubuntu runners as well, now supporting hardware acceleration. This means we no longer need MacOS. The Ubuntu runner is also about 2 times faster.
2024-04-27 10:05:58 +02:00
flofriday 4cf362393a
Fix infinite refresh indicator (#7137)
Before when refreshing any feed(s) without network the refresh indicator
stayed indefinitely.

This was also the case if you were on mobile, trying to refresh a need
and in the popup selected "don't update over mobile".
2024-04-25 22:42:23 +02:00
flofriday 4bc0b38280
Implement missing equals and hashcode methods for feeditem (#7132)
Till 5713b18267 many classes like FeedItem
used to inherit from FeedComponent which provided those two methods.
However since that commit the component no longer exists and now the
classes need to implement it on their own. Without this, ArrayList.remove breaks.
2024-04-24 21:06:12 +02:00
flofriday 7b048ed579
Make contributors clickable (#7129) 2024-04-24 20:57:29 +02:00
flofriday c56facd141
Improve about icons (#7122)
The new icons better represent the contributors and the privacy policy.
2024-04-21 19:50:19 +02:00
hades 841bda020f
Do not enable sleep mode in Android Auto (#7053)
When playback is started while an Android Auto projection is active, we want to
prevent automatic sleep timer from starting.

Note: the androidx.car.app library has not seen a full release since 1.2.0. We opted to use a release candidate here, which has a downgraded minSdk requirement, compatible with the current minSdk of AntennaPod at the time this dependency is introduced.
2024-04-17 00:01:34 +02:00
ByteHamster 0aa8e85003 Bump version to 3.4.0-beta2 2024-04-16 22:53:10 +02:00
Tom Hense 2f58b4b360
Strip duplicate slash on Nextcloud Gpodder sync (#7085) 2024-04-16 08:14:59 +02:00
ByteHamster 5e7858ef7e
Show covers on podcast page (#7094) 2024-04-15 19:28:10 +02:00
ByteHamster 2043e71299
Show feed search results as soon as they are available (#7100)
without waiting for episode search results
2024-04-15 19:24:06 +02:00
ByteHamster 91bcf4b400
Work around race condition where position reset might be undone (#7102)
When the position saver ticks while the service is just about to be
stopped, it might happen that we first reset the position and then
set it to the end again. This works around this.
2024-04-15 19:23:26 +02:00
ByteHamster 8037bd2239
Fix default per-feed skip silence setting (#7101) 2024-04-15 19:22:07 +02:00
ByteHamster e9b3cc34fe
Optionally display subscriptions as a simple list (#7087) 2024-04-14 11:45:12 +02:00
ByteHamster d6b2a49caa
Hide info views in multi-select (#7095)
Still not perfect because the toolbar is visible behind the action menu.
However, it fixes the jumping when entering multi-select mode.
2024-04-14 11:43:50 +02:00
ByteHamster f3bca9d9e4
Add lazy loading to feed item list (#7091) 2024-04-13 19:18:13 +02:00
ByteHamster 04fab47072
Store download date in database (#7090) 2024-04-13 17:28:56 +02:00
ByteHamster 456159e85f
Fix detection of local-only refresh (#7088) 2024-04-13 10:04:03 +02:00
ByteHamster 25e4703da4
Fix website languages being in wrong folder (#7084) 2024-04-12 23:52:35 +02:00
ByteHamster 863d4c3b61
Don't spam the logs when doing unit tests (#7081) 2024-04-11 23:50:25 +02:00
ByteHamster 58db8f1032 Bump version to 3.4.0-beta1 2024-04-11 23:02:20 +02:00
ByteHamster 80ea632da3 Update contributors 2024-04-11 23:02:06 +02:00
ByteHamster 1a92db4706 Update translations 2024-04-11 22:57:42 +02:00
ByteHamster d9e84f8c38
Target SDK 34 (#7075) 2024-04-09 22:33:52 +02:00
ByteHamster bd4e9e19d7
Don't allow downloading already downloaded episdoes again (#7076) 2024-04-09 22:33:31 +02:00
ByteHamster e578f4ca93
CI tweaks (#7069)
- Run Checkstyle with gradle to make it easier for users
  - No longer needs different configuration for new code
  - Exclude current violations
  - Fix some violations that somehow couldn't be specified in the exclusion file
- Print SpotBugs/Lint/Checkstly violations in GitHub format
  - Then the CI run gets annotated on the web UI
2024-04-07 23:28:14 +02:00
ByteHamster fc40da28a7 Merge branch 'master' into develop 2024-04-07 10:42:25 +02:00
ByteHamster e4bac5ea71
Do not crash if an event comes in before the media is loaded (#7067) 2024-04-06 20:24:47 +02:00
Fredrik Wallén 00d6df6261
Make it possible to sort the home screen (#7048) 2024-04-05 20:45:26 +02:00
ByteHamster 687db0f5ed
Merge :net:sync:model and :net:sync:service-interface (#7063) 2024-04-05 20:08:25 +02:00
Taco b6a4049ff4
Spotbugs cleanup (#6968)
Remove unused SpotBugs rules.
Fix URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD, ICAST_INTEGER_MULTIPLY_CAST_TO_LONG, NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION, OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE
2024-04-05 19:28:36 +02:00
ByteHamster 92ab575b15
Delete core module (#7060) 2024-04-05 19:20:27 +02:00
ByteHamster 2143ab1351
Move some tests from core module to their respective module (#7059) 2024-04-04 22:26:53 +02:00
ByteHamster 0288d4e51e
Small database efficiency tweaks (#7058)
- When checking whether there is a subscription, there is no need to create feed objects (plus counters etc). Just the number of episodes is enough.
- Downloads section only needs to load the items it actually displays.
- No need to load FeedMedia, just to load FeedItem including the same FeedMedia afterwards.
- No need to convert columns to Strings and back to Longs.
- No need to join favorites when we are only interested in the list of IDs anyway.
2024-04-04 21:58:36 +02:00
ByteHamster e894ff1ccb
Remove methods from DbReader that just call a private method (#7057) 2024-04-04 21:25:57 +02:00
ByteHamster 613a9896e9
Remember column indices between different list items (#7051)
This is way faster than searching for the column index again for every item.
2024-04-03 22:21:42 +02:00
ByteHamster a846e417b0
Fix playback state not being updated (#7050) 2024-04-01 09:55:30 +02:00
ByteHamster edb440a5a9
Restructure related UI classes together (#7044) 2024-03-31 18:40:15 +02:00
ByteHamster 4e47691e70
Remove gpodder search (#7047)
The search results are usually broken anyway
or the server just returns an error 500
2024-03-31 09:15:53 +02:00
ByteHamster 86ff7f540b
Remove unneeded module dependencies (#7046) 2024-03-31 09:15:03 +02:00
loucasal bf1bd56186
Update string to sentence case (#7045) 2024-03-30 11:19:41 +01:00
ByteHamster d76b6f63ee
Fix search button not working on some screens (#7043) 2024-03-29 21:49:53 +01:00
ByteHamster 8accb54685
Move playback service to module (#7042) 2024-03-29 21:05:02 +01:00
ByteHamster 2fd73b148d
Move download service to module (#7041) 2024-03-29 19:27:53 +01:00
ByteHamster 6f3a9b1676
Create module for sync service and move DBWriter to database module (#7040) 2024-03-29 17:45:14 +01:00
ByteHamster 0c8c9a89a3
Move about screen to :ui:preferences (#7039) 2024-03-29 13:39:45 +01:00
ByteHamster f9dd837362
Remove ClientConfig class (#7038) 2024-03-29 13:39:19 +01:00
ByteHamster 8f553f08f0
Add :ui:discovery module (#7037) 2024-03-29 13:38:31 +01:00
ByteHamster 5ede21d676
Remove dependency of :net:discovery and :ui:echo on :core (#7036)
Moves the common icon files to :ui:common
2024-03-29 11:23:33 +01:00
ByteHamster 13a985ca1e
Restructure Echo to be more flexible (#7035)
Each screen is its own file, which makes it easier to add interactive elements.
2024-03-29 08:55:13 +01:00
ByteHamster 1dbda2fb8a
Split up DBTasks which has unclear responsibilities (#7032) 2024-03-27 21:01:45 +01:00
ByteHamster 130da46f5d
Move widget setup code to widget module (#6996) 2024-03-25 23:45:09 +01:00
ByteHamster 160089d3ff
Add script to generate module diagram (#7028) 2024-03-25 23:28:50 +01:00
ByteHamster 69b24699a3
Move DBReader to :storage:database (#7027) 2024-03-25 21:45:43 +01:00
ByteHamster 15eab50223
Move debug icons to :ui:common (#7026) 2024-03-24 22:07:18 +01:00
ByteHamster 5c6000155c
Let the database do the sorting (#7025) 2024-03-24 21:27:30 +01:00
ByteHamster 4078b3475e
Simplify playback preferences and move to :storage:preferences (#7024) 2024-03-24 21:08:06 +01:00
ByteHamster 7b390f1c92
Speed up feed parsing (#7023)
AntennaPod is quite slow with huge feeds. The reason is that we have a bunch of workarounds for misbehaving feeds that also make it slower to work with feeds that do not misbehave.

Changes:

- Only start guessing duplicate episodes when no "proper" match is found
- Only parse non-html as HTML for attributes that really need it
- Do not log failed Long parsing when size is not specified
- Try to parse dates with RFC822 first before falling back to workarounds for other formats

I ran a benchmark with "Stuff you should know" (for which the workarounds are not needed) containing 2k episodes. Includes download of 8MB of feed XML (~5 seconds), debug build.
Before: 44 seconds, after: 13 seconds ==> 3.4 times faster feed refresh
2024-03-24 18:04:39 +01:00
ByteHamster 701b1ce339 Bump version to 3.3.2 2024-03-24 17:58:24 +01:00
ByteHamster 084b9c2317
Store last refresh attempt for feeds (#7022) 2024-03-24 17:57:00 +01:00
ByteHamster 84b0a79d8c
Fix missing episodes on Android Auto (#7021) 2024-03-24 12:25:52 +01:00
ByteHamster 5218e06904
Faster feed deletion (#7019) 2024-03-24 12:25:32 +01:00
ByteHamster 79856b7931
Launch splash activity after restoring backup (#7020) 2024-03-24 01:38:10 +01:00
ByteHamster a065d3fc33
Remove check for updated attributes, just update them (#7018) 2024-03-23 18:06:02 +01:00
ByteHamster f6b45e7162
Rename FeedMedia methods to no longer have underscores (#7017) 2024-03-23 11:27:55 +01:00
ByteHamster f20ce1fc69
Move first batch of preferences code to :ui:preferences (#7010) 2024-03-23 09:40:40 +01:00
ByteHamster 376c83d200
Fix loading chapter images in local feeds (#7016) 2024-03-22 22:12:36 +01:00
ByteHamster 69f0daa2e8
Fix android:pathPattern not starting with a slash (#7014) 2024-03-22 20:10:33 +01:00
ByteHamster ab64807f64
Remove AutoDownloadTest (#7015)
This test regularly fails our CI.

The test checked that auto-download kicks in after the currently playing episode
and that it considers the correct item in the queue to enqueue after.

However, because we now use WorkManager, the download can be delayed based
on decisions by the Android system. We cannot assume that downloading already
starts just seconds after playback completes.

I do not know an easy fix for this, and the test is quite complex anyway, testing
multiple different modules at once. So I am removing the test for now.
2024-03-22 20:10:15 +01:00
ByteHamster bd17373c18
Playback speed fixes (#7013)
- Remove video-specific playback speed (no longer needed now that we have per-podcast speed)
- Respect changed speed setting on settings page even if the service is not running
- Do not change global speed when feed setting is updated
2024-03-22 19:44:14 +01:00
ByteHamster 0a6b7ed699
Nicer rating dialog (#7011) 2024-03-22 18:18:30 +01:00
ByteHamster c71e86f427 Bump version to 3.3.1 2024-03-20 20:34:22 +01:00
ByteHamster f0e685c5a9 Update translations 2024-03-20 20:32:52 +01:00
ByteHamster ac8e8137bb
Catch quick settings tile exceptions (#7006)
The exception gets thrown if AntennaPod is installed in a work profile.
2024-03-20 20:30:45 +01:00
ByteHamster 27aa5cba96
Suppress outdated dependency Lint (#7009) 2024-03-20 20:30:28 +01:00
ByteHamster 542d50cba7
Create automatic backups only every 3 days, update summary (#7005) 2024-03-20 18:45:39 +01:00
ByteHamster 4bc90897b6
Only consider backup files with the exact same filename pattern for deletion (#7004) 2024-03-20 00:08:17 +01:00
ByteHamster 53f68ca260
Make swipe gestures less slippery (#7003)
The sine function made the item move faster than the finger.
2024-03-20 00:08:04 +01:00
ByteHamster 55845c46a1
Optional automatic daily database backup (#6994) 2024-03-18 07:36:37 +01:00
ByteHamster d40b9ef59b
Decouple media button starter and receiver (#6999) 2024-03-18 07:28:17 +01:00
ByteHamster 2d77b1f118
Remove dependency from :ui:glide to :core module (#6998) 2024-03-17 20:25:44 +01:00
Taco b84a05bd4e
Add POST_NOTIFICATIONS permission checks (#6951)
Also update AndroidX Core to 1.9.0 because then `checkSelfPermission()` delegates to `areNotificationsEnabled()` if needed
2024-03-17 19:58:33 +01:00
ByteHamster 0cbd97b5cb
Move theme to :ui:common module (#6997)
This enables creating Activities outside the app and core modules
2024-03-17 18:43:55 +01:00
ByteHamster 17f5a5d1b8
Move notification icons and widget icons to separate modules (#6995) 2024-03-17 12:06:41 +01:00
Tony Tam 8dc8cc64a8
Allow retrying chapter loading if interrupted (#6828)
Chapter loading can sometimes get interrupted, most importantly if
the corresponding fragment tries to refresh the view again.
Before, this set the chapters to an empty list, indicating that it
should not be tried again. Now, interrupted exceptions do not set
the list to be empty, so it can be retried later.
2024-03-17 11:52:16 +01:00
Taco 48c0ccb4a2
Fix "add podcast" FAB still visible under SpeedDial main FAB (#6950) 2024-03-17 11:49:56 +01:00
ByteHamster da21d92f96 Fully remove string that fails Lint 2024-03-13 22:39:21 +01:00
ByteHamster afc21f46a9 Remove translation that fails Lint 2024-03-13 22:22:37 +01:00
ByteHamster 7d89b18afb Merge branch 'master' into develop 2024-03-13 21:59:49 +01:00
Taco 030226f288
Replace SwitchCompat with MaterialSwitch (#6989) 2024-03-13 20:14:07 +01:00
ByteHamster 2f3f1fd186
Move import/export to its own module (#6986)
Also clean up ImportExportPreferencesFragment a bit.
2024-03-11 23:10:09 +01:00
ByteHamster 8177875674
Fix opml elements not showing title (#6988) 2024-03-11 23:09:00 +01:00
ByteHamster 0848364810 Bump version to 3.3.0 2024-03-10 21:14:59 +01:00
ByteHamster 44e123105c Backport: Remove tab characters from last 6 files 2024-03-10 20:57:32 +01:00
ByteHamster 635e6c8267 Update translations 2024-03-10 20:34:09 +01:00
ByteHamster 2e9fcc044f Update release notes 2024-03-10 20:32:14 +01:00
ByteHamster 5c98a33ed2
Remove Jetifier (#6982)
All the dependencies we use now support androidx
2024-03-10 10:47:54 +01:00
ByteHamster baa58ac17f
Remove wearable support library (#6978)
The library is the last one requiring Jetifier
and we only use 3 string constants in that whole library anyway.
2024-03-10 10:16:13 +01:00
ByteHamster 095a6b3e9d
Remove stream library that is just used in 3 locations (#6976)
Especially on the SwipeActionsDialog, this is even a bit easier to understand.
2024-03-10 10:14:17 +01:00
ByteHamster 393a8cebd3
Remove last few usages of Iconify (#6977) 2024-03-10 08:02:34 +01:00
Taco b18e5f0de6
Fix some Media3 deprecations (#6980) 2024-03-10 07:59:53 +01:00
Taco f1fe1b573f
Fix OkHttp deprecations (#6979) 2024-03-10 07:55:35 +01:00
ByteHamster 48e8197e3f
Upgrade OkHttp (#6975) 2024-03-09 19:44:05 +01:00
ByteHamster aaf225c7af
Remove okio library (#6972) 2024-03-09 17:55:07 +01:00
ByteHamster 755ccc42ec
Upgrade ExoPlayer to media3 version (#6971) 2024-03-09 17:52:21 +01:00
ByteHamster 39e2d6e230
Move Google Play Metadata to its own repo (#6970)
This avoids having hundreds of megabytes of screenshot data in the main repo every single time we re-generate them. Then developers do not have to clone a huge repo (at least if they clone without submodules). It also enables rewriting the screenshot git history to be smaller without rewriting the code git history (which would be quite a bad idea).
2024-03-09 11:02:23 +01:00
Taco e1ef2a643a
Update AndroidX libraries (#6940) 2024-03-09 10:56:58 +01:00
ByteHamster e8807bb329
Fix importing opml file with empty url and title (#6966) 2024-03-07 22:54:34 +01:00
Taco b2718a9a12
Delete unused TriangleLabelView license (#6965) 2024-03-07 19:03:48 +01:00
peking_ling b4a6203e1a
Convert teaser from png to webp for smaller file size (#6959) 2024-03-07 07:23:20 +01:00
ByteHamster 7c14534179
Remove TriangleLabelView (#6963)
Since we redesigned the subscriptions page, this class is unused.
2024-03-06 21:53:40 +01:00
ByteHamster 68ec4e2527
Merge pull request #6958 from TacoTheDank/bumpSpotBugs
Update and fix SpotBugs
2024-03-06 17:46:05 +01:00
TacoTheDank 522288260c Clean up some old SpotBugs rules 2024-03-06 02:55:01 -05:00
TacoTheDank c2ccc28b95 Update SpotBugs 2024-03-06 02:52:14 -05:00
TacoTheDank 6f582e4c52 Fix SpotBugs in CI 2024-03-06 02:32:13 -05:00
ByteHamster 5e8960f4bc
Merge pull request #6955 from ByteHamster/checkstyle
Move some Checkstyle rules from new-code file to main file
2024-03-06 07:32:24 +01:00
ByteHamster cae848b505 Fix indentation in last 8 files 2024-03-04 23:09:59 +01:00
ByteHamster 6c0f9eec62 Remove tab characters from last 6 files 2024-03-04 23:09:55 +01:00
Taco 40da13e014
Clean up some dead code (#6952) 2024-03-04 23:07:28 +01:00
ByteHamster c21edc8b79 Fix Gradle Checkstyle 2024-03-04 22:36:10 +01:00
Taco c06a3a6d27
Update AGP and Gradle (#6954) 2024-03-04 22:17:44 +01:00
quails4Eva 60f3d77eb2
Skip silence setting per feed (#6910) 2024-03-03 20:17:22 +01:00
ByteHamster 3c77d43e76
Specify foreground service type (#6953) 2024-03-03 13:00:00 +01:00
ByteHamster ee99ef934c
Remove FeedComponent and FeedFile class (#6949)
We want to be more flexible in what we store for each type of item. Also rename misleading function (lastUpdate to lastModified)
2024-03-02 09:50:24 +01:00
peking_ling fa9dd8cb5a
Cache streamed media files on disk (#6927) 2024-02-29 21:02:48 +01:00
ByteHamster 33569e8992 Tweak issue labels and PR template 2024-02-28 22:48:08 +01:00
Matej Drobnič 7332c04631
Add option to add new episodes to queue (#6855) 2024-02-25 16:11:30 +01:00
ByteHamster a7068cc24a
String tweaks reported on Transifex (#6942) 2024-02-25 15:31:59 +01:00
mueller-ma 9cfbae183c
Toggle sleep timer from notification (#6913) 2024-02-25 15:02:44 +01:00
ByteHamster 82c93bf7ee
Guess next episode release date (#6925) 2024-02-25 14:01:03 +01:00
Taco ef4af0d29d
Fix Gradle deprecations (#6939) 2024-02-25 13:39:44 +01:00
loucasal 55c72097b0
Fix warnings about deprecated checks (#6935) 2024-02-24 15:05:15 +01:00
ByteHamster 45a05ed332
Screenshot creation script (#6933) 2024-02-20 21:17:23 +01:00
ByteHamster 3b2e7420cd
Remove some deprecated methods (#6932) 2024-02-20 21:15:55 +01:00
ByteHamster 22f36bc9c0 Bump version to 3.3.0-beta2 2024-02-19 00:00:45 +01:00
ByteHamster 7a40a505f3 Merge branch 'develop' 2024-02-18 23:59:46 +01:00
ByteHamster dc63386e89 Bump version to 3.3.0-beta1 2024-02-18 21:15:41 +01:00
ByteHamster e5f564be94 Update contributors 2024-02-18 19:31:20 +01:00
ByteHamster d7572e4de4 Update translations
New languages >40% translated:
Greek, Hindi, Sardinian
2024-02-18 19:28:46 +01:00
ByteHamster d9ebf42167 Translation fixes 2024-02-18 19:25:50 +01:00
ByteHamster 0d29e44de5 Merge branch 'master' into develop 2024-02-18 19:05:13 +01:00
mueller-ma 556597a173
Rewind when sleep timer pauses playback (#6923) 2024-02-18 19:02:33 +01:00
ByteHamster c7c5ab567b
Use proper plurals when showing number of episodes (#6922) 2024-02-16 23:50:12 +01:00
ByteHamster c07ae17962
Tweak OPML import (#6906)
- Only request storage permission when ContentResolver fails
- Easier to read error message
2024-02-04 22:10:12 +01:00
Matej Drobnič 0f5600932d
Add next chapter button to notification (#6276) 2024-02-04 19:54:46 +01:00
ByteHamster f0e96a2692
Remove unused test class (#6907) 2024-01-31 21:48:39 +01:00
ueen 34fb2050b2
Hide refresh from toolbar (#6850) 2024-01-20 17:31:16 +01:00
ByteHamster 6e2a8b86a7
Merge pull request #6859 from ByteHamster/echo-tweaks 2024-01-10 17:12:31 -05:00
ueen b1e6da935b
Always show share in player toolbar (#6849) 2024-01-04 19:10:58 +01:00
ByteHamster 0361e05ca8 Permanently hide Echo section if it has too few hours 2024-01-04 18:59:06 +01:00
ByteHamster 9eac993e45 Center numbers on Echo share screen 2024-01-04 18:58:59 +01:00
ByteHamster c8230b7034 Ellipsize titles on Echo share image 2024-01-04 18:22:42 +01:00
ByteHamster 3410d79eb2 Increase timeout of Echo images 2024-01-04 18:16:36 +01:00
ByteHamster bf67218422
Print duration as number of days only on Echo (#6842)
Reverts an accidental change to the queue time display
2024-01-03 20:32:56 +01:00
ByteHamster 4a782e457c
Update queue list when toggling 'keep sorted' (#6853) 2024-01-02 20:18:20 +01:00
quails4Eva c5093c9ff9
Move 'skip silence' checkbox to playback speed dialog (#6834) 2024-01-02 20:17:29 +01:00
ueen f1e91f9d8b
Migrate subscriptions filter dialog to DialogFragment (#6846)
Co-authored-by: ByteHamster <info@bytehamster.com>
2024-01-02 19:56:57 +01:00
ByteHamster b2ea588b54
Use localized date format (#6843) 2024-01-02 19:21:07 +01:00
satish-vanjara de8bc4ad30
Add scrollbar to Nextcloud login dialog (#6838) 2024-01-01 12:40:43 +01:00
ueen c81157f0e6
Add reset button to episodes filter (#6825) 2023-12-31 11:32:02 +01:00
ByteHamster 8c7d567a0c
If feed url was typed manually, show edit url button on error (#6833) 2023-12-31 11:09:12 +01:00
ByteHamster 28edb71fd6
Share AntennaPod subscribe link instead of RSS url (#6835)
Apparently users are confused by RSS links.
2023-12-31 11:08:30 +01:00
ByteHamster 9db26b7bab
Remove unnecessary autodownload code (#6832)
This should not change any behavior.
The retry count and timing are managed by WorkManager, so this code is irrelevant.
2023-12-29 19:25:39 +01:00
Tony Tam 7508e15ab1
Show currently playing episode in Android Auto (#6816) 2023-12-29 17:50:31 +01:00
Matej Drobnič f476086114
Check if volume boost effect is supported on the device (#6808) 2023-12-29 17:15:21 +01:00
ByteHamster 55f83eb9e1
Close unresponsive tickets earlier (#6830) 2023-12-29 00:07:53 +01:00
ByteHamster 4e4b6062ac
Better center number in 'new' pill (#6831) 2023-12-29 00:07:21 +01:00
ByteHamster d39ddaa113
Fix Echo using wrong number of days in a year (#6822) 2023-12-28 20:15:26 +01:00
ByteHamster b066c6e23c
Update preference search library (#6821) 2023-12-23 20:58:12 +01:00
ByteHamster db88dc10e6
Make it easier to migrate Echo to a new year (#6803)
Also, add a new screen background
2023-12-13 22:40:51 +01:00
ByteHamster 3852d50f92
Fix 'played' state on TalkBack when cover is hidden (#6796) 2023-12-06 21:26:49 +01:00
ByteHamster ae4205c6d3 Merge branch 'master' into develop 2023-12-06 21:02:53 +01:00
ByteHamster 2e76dc8d0c
New sort dialog (#6789) 2023-12-03 16:36:28 +01:00
ByteHamster c1712fe2f5
Update debug icons with new design (#6782) 2023-12-02 10:45:00 +01:00
ByteHamster 1caffa70f7
Remove audio player license (#6783) 2023-12-02 10:44:44 +01:00
ByteHamster 58081fe5bf Bump version to 3.2.0 2023-11-29 08:35:43 +01:00
ByteHamster 636d705e8f Fix Italian translations 2023-11-28 20:46:29 +01:00
ByteHamster 37ad5d490b Update translations 2023-11-28 20:37:25 +01:00
ByteHamster ee554d0306
AntennaPod Echo (#6780) 2023-11-28 20:26:29 +01:00
Andrey Gusev b792eaa18e
Make onPlayFromSearch continue playback (#6779)
According to the Android documentation, if onPlayFromSearch is called with an empty query, the app should make a decision what to play. Before, a database search with this empty query was performed, which returned arbitrary results. Now we play the last played episode instead.
2023-11-28 19:50:49 +01:00
ByteHamster 6177cc2460
De-duplicate also if episodes have different but similar media type (#6776) 2023-11-26 11:15:14 +01:00
caoilTe O'Connor 95f431fec9
Remove Iconify from FeedInfoFragment (#6655) 2023-11-22 20:29:58 +01:00
peking_ling 45480f4e2c
Add ScrollView to sleep timer dialog 2023-11-17 22:51:42 +01:00
Tony Tam c7d6cd358c
Honor sort in episode list view in Android Auto (#6756) 2023-11-17 22:33:16 +01:00
Erik Johnson 637230e382
Fix seeking to end using seek bar (#6763)
Merging #6074 has caused a new edge case for VBR audio files, in which
using the seek bar to seek to the end of an episode sometimes hits the
new code path, and the `skip()` function is called.

Because `skip()` invokes `endPlayback()` with `hasEnded` set to `false`,
post-processing tasks are not executed unless the pre-seek position
falls within the "Smart mark as played" range. If "Smart mark as played"
is set to `Disabled`, or the pre-seek position is outside that range,
then the episode is not marked as played, and not removed from queue.

This commit fixes that edge case by replacing `skip()` with a direct
call to `endPlayback()`, with `hasEnded` set to `true`.
2023-11-15 20:47:51 +01:00
ByteHamster 0bb4870820
Be more aggressive about telling users to also search closed issues (#6762) 2023-11-14 21:12:03 +01:00
ByteHamster 10672f8086 Bump version to 3.2.0-beta2 2023-11-13 22:53:58 +01:00
ByteHamster 1e3761984a Update contributors list 2023-11-13 22:53:42 +01:00
ByteHamster 956a455f84 Update translations 2023-11-13 22:48:30 +01:00
ByteHamster 46c3d4e8c1
Fix file deletion (#6758)
- When deleting local folders, don't delete files
- Don't try to delete files twice
- Fix deleting non-local feeds
2023-11-12 23:00:06 +01:00
Tony Tam 7bfb53cc00
Fix sometimes stopping at the end of each episode (#6753)
The bug is on this line [#145](f7a13065a9/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java (L145)) - the call to `PlaybackPreferences.getCurrentlyPlayingFeedMediaId()` returns the episode that was playing and deleted, hence it stops playing the next episode like it is supposed to.  Because it's called in a Thread, the next episode already started playing for 1 second or so and then stops

The fix will now save into the preference the correct episode that is playing on the PREPARE stage.
2023-11-11 10:55:01 +01:00
ByteHamster 8af06a9f25
Fix deeplink/search?query=xy intent (#6754) 2023-11-11 10:52:16 +01:00
Matej Drobnič 4d627cc3af
Only set target gain when enhancer was enabled (#6751) 2023-11-09 20:26:43 +01:00
ByteHamster 47761bf98f Bump version to 3.2.0-beta1 2023-11-05 16:14:41 +01:00
ByteHamster 0a6a3d6854
Add button to share exported database export (#6746)
Also hide path, is not accessible on recent Android versions anyway.
2023-11-05 16:06:20 +01:00
ByteHamster 34c7fd576f
Show 'copied to clipboard' message on Android 12L (#6745) 2023-11-05 15:48:35 +01:00
ByteHamster 1d415c9f7f
Announce played/unplayed in TalkBack (#6744) 2023-11-05 15:31:07 +01:00
ByteHamster f7a13065a9 Merge branch 'master' into develop 2023-11-05 08:27:34 +01:00
Harshad Vedartham 2c3fb5610a
Do not reinit on pause (#6732)
The call to `reinit()` causes Android Auto to close the player view when pausing
2023-11-01 15:56:28 +01:00
Vinod Patil 691ed73910
Fix adding new playback speed preset when service is not running (#6734) 2023-11-01 14:39:10 +01:00
Bhaskar Kaura 01f1927770
Add more spacing to OnlineFeedViewActivity, use MD3 styles (#6670) 2023-10-29 16:13:51 +01:00
ByteHamster 4931734d94
Allow hiding notification permission nag (#6730)
- Support showing most error messages as a snackbar
- Ask for notification permission when enabling episode notifications
- Clarify what we use notifications for
2023-10-29 16:10:38 +01:00
Erik Johnson 8a011badd3
Fix fast-forward at end of episode (#6074)
When using variable speed, skipping back and forth introduces some
uncertainty to the current position, causing skip-forward to try to skip
to an invalid position when very near the end of the episode. This
change fixes this by skipping the current episode if the desired
skip-forward position exceeds the duration.
2023-10-29 09:43:20 +01:00
Matej Drobnič 346365b8d0
Delete local feed episodes (#6400) 2023-10-22 16:53:41 +02:00
ByteHamster fa75317bce
Do not try to sync local feeds (#6722) 2023-10-22 16:51:16 +02:00
ByteHamster 69be89881a
Make 'change times' button a settings icon (#6721)
Users didn't scroll down to find the button. Now it is next to the checkbox,
so it is harder to overlook.
2023-10-22 15:09:07 +02:00
ByteHamster 0b7403e1dd
Some more accessibility tweaks (#6713) 2023-10-22 13:14:36 +02:00
Vinod Patil 61669d32fa
Multi-select actions on search results (#6719) 2023-10-22 13:07:12 +02:00
ByteHamster 8d4270ab87 Bump version to 3.1.2 2023-10-22 12:52:25 +02:00
ByteHamster eb8267a4ae Update translations 2023-10-22 12:39:31 +02:00
Taco e9d190da1b
Bump AGP and Gradle (#6489) 2023-10-17 20:52:21 +02:00
Keunes 77483913d4
Remove periods for single sentences (#6707) 2023-10-15 10:52:34 +02:00
ByteHamster 0efa91a0b1
Add error icon on subscriptions screen (#6679) 2023-10-15 10:26:13 +02:00
ByteHamster 475e0f5128
Remove ProgressBar background (#6703)
Might lead to bad contrast in dark theme when applying dynamic colors
2023-10-13 17:31:39 +02:00
ByteHamster 58484d5790
Add 'Search online' button if local search has no results (#6681) 2023-10-13 17:31:09 +02:00
ByteHamster 2ee2cb6702
Ignore skip in first second of playback (#6704)
Users complained that they wanted to skip the ending of
an episode and accidentally skipped the next one that
started while their finger was moving.
2023-10-13 00:26:07 +02:00
ByteHamster c732ecba8b
User material3 dialog in 'mobile updates' setting (#6682) 2023-10-12 18:13:55 +02:00
ByteHamster c38b263458
Better content descriptions for TalkBack (#6684) 2023-10-06 18:24:13 +02:00
ByteHamster 3fae29b375
Show playback speed in dialog even when not playing (#6685) 2023-10-06 08:18:17 +02:00
ByteHamster da200f6139
Fix crash if vorbis exception does not have message (#6678) 2023-10-02 21:12:45 +02:00
ByteHamster 4dc1196c39
Apply username and password when subscribing (#6675) 2023-10-01 18:08:54 +02:00
ByteHamster 7cb0ba8156
Switch to colorBackground instead of windowBackground (#6674) 2023-10-01 18:07:47 +02:00
ByteHamster 7e8ac3aeb6
Fix stuck notification when refreshing single feed (#6662)
Updating a singletonList throws an UnsupportedOperationException,
so the worker does not remove the notification
2023-10-01 14:21:19 +02:00
blair e466bba013
Handle double/triple headset button press (#6535) 2023-09-30 12:23:58 +02:00
Rahmat Ramadhan 922395a448
Sort downloads by size (#6659) 2023-09-27 21:59:58 +02:00
ByteHamster 7229cb40e9
Don't update redirect url if it is the same as the original (#6661) 2023-09-27 21:54:50 +02:00
caoilTe O'Connor 574ec1434c
Remove Iconify from SubscriptionFragment (#6645) 2023-09-24 13:10:28 +02:00
ByteHamster 0e52f08aa5
Fix file deletion and queueing after download (#6652)
WorkManager does not tell us whether it was cancelled by
the user (not retried) or by the system (retried later).
So we need to delete the file and remove from queue when
we know that it was actually the user. Also make sure
to always delete the file when the download fails.

Also, don't show "will retry" message on last retry attempt.
2023-09-24 10:03:50 +02:00
caoilTe O'Connor 705aae44ba
Remove Iconify from NavListAdapter (#6578) 2023-09-16 12:34:30 +02:00
ByteHamster 8073de55af Bump version to 3.1.1 2023-09-12 20:09:34 +02:00
ByteHamster c680f84a0f Update translations 2023-09-12 19:58:55 +02:00
ByteHamster b933c0eb71
Refresh local feeds even if there is no internet (#6633) 2023-09-12 19:53:42 +02:00
ByteHamster 955fca6e38
Improve wording (#6604) 2023-09-12 19:23:39 +02:00
ByteHamster 37c29a6372
Remove auto-download notification setting (#6625)
Also, add episode notifications to multi select options
2023-09-12 19:19:28 +02:00
ByteHamster 1e7c347cd2 No longer use 'metered' work-around for VPNs
WorkManager doesn't do the workaround either.
So we would launch a download that then never starts.
2023-09-12 19:18:12 +02:00
ByteHamster e4df6222c2
Make preference switch background less prominent (#6623) 2023-09-08 22:11:12 +02:00
ByteHamster ce7cffdbf3
Open inbox instead of 'all' from new episodes notification (#6624) 2023-09-08 22:05:19 +02:00
ByteHamster b7491e5e71
Do not cache AVD (#6626)
This causes tons of hard-to-reproduce bugs on CI.
2023-09-08 21:09:07 +02:00
Matej Drobnič 8ebf153970
Add volume boost (#6573) 2023-09-08 13:28:20 +01:00
ByteHamster 9ed5485ae3
Restore Nextcloud login state when recreated (#6600) 2023-08-28 17:27:53 +02:00
Keunes 3564484c2c
Merge pull request #6599 from ByteHamster/issue-template-fdroid
Try to shoo away the F-Droid people who create new issues for every single release
2023-08-26 07:42:03 +02:00
ByteHamster 1baa8e688b Try to shoo away the F-Droid people who create new issues for every single release 2023-08-25 10:51:08 +02:00
ByteHamster 087770026f Update release notes 2023-08-24 21:05:05 +02:00
ByteHamster b5cd973a86
Do not try to resume download of feeds (#6591) 2023-08-22 12:00:34 +02:00
ByteHamster 91d5238f08 Bump version to 3.1.0 2023-08-19 11:47:52 +02:00
ByteHamster 823907bf1f Update translations 2023-08-19 11:47:51 +02:00
ByteHamster 4c9db040fe Update contributors 2023-08-19 11:24:06 +02:00
ByteHamster 3ce3219a3b Do not crash if item is not loaded yet 2023-08-19 11:21:52 +02:00
ByteHamster 056d262ab5 Fix ClassCastException when showing error message on video player 2023-08-19 11:21:52 +02:00
ByteHamster 18ab4ab8c6 Fix race condition in progress notification 2023-08-19 11:21:52 +02:00
ByteHamster 4182f83367
Sync on app start (#6589) 2023-08-19 10:59:12 +02:00
ByteHamster 4f6b563e3f
Avoid race conditions when updating download notification (#6588) 2023-08-19 10:46:43 +02:00
ByteHamster 49ac7a83b8
Relayout NoRelayoutTextView if text gets longer (#6587) 2023-08-19 10:46:17 +02:00
ByteHamster ca9358234f Bump version to 3.1.0-beta3 2023-07-18 17:58:28 +02:00
ByteHamster 9f8edd0e9d Fix MediaMetadataRetriever on API<29
On SDK<29, this class does not have a close method yet, so the app crashes when using try-with-resources.
2023-07-18 17:57:40 +02:00
ByteHamster 9be6562b4e Fix missing foreground notification on old Android versions 2023-07-18 17:57:40 +02:00
ByteHamster 5ae766b1a1 Bump version to 3.1.0-beta2 2023-07-16 22:23:25 +02:00
ByteHamster 196ff13442
Upgrade preferences before using them (#6546) 2023-07-16 22:22:25 +02:00
ByteHamster 019e3574c4 Fix translator breaking checks 2023-07-16 11:50:26 +02:00
ByteHamster 1a0134d5f2 Bump version to 3.1.0-beta1 2023-07-16 10:40:03 +02:00
ByteHamster c99ed8b48a Update translations 2023-07-16 10:40:00 +02:00
ByteHamster fa12968ae5
Fix crash in item pager (#6542)
There should be no code path for feedItemPos to still be -1, but the
crash reports indicate that it does. So this is now the dirty fix to
avoid app crashes.
2023-07-15 22:04:11 +02:00
ByteHamster f1f3674230
Fix Chromecast crash if FeedItem does not have a feed (#6541) 2023-07-15 22:03:50 +02:00
ByteHamster 8d1eb62f0b
Delete partially downloaded file when giving up to retry (#6530) 2023-07-15 16:27:12 +02:00
ByteHamster 75c3c4cf24
Don't allow adding items without media to the queue (#6529) 2023-07-15 15:55:24 +02:00
ByteHamster 6999a944bb
Fix 'allow once' for feed update on mobile networks (#6528) 2023-07-04 22:31:47 +02:00
ByteHamster 23d4cf5632
Merge pull request #6490 from TacoTheDank/minSdk21removals
Remove no longer needed Compat stuff
2023-06-24 14:45:07 +02:00
Manjeet Yadav 192d71c7ab
Fix mini player in landscape mode missing the Play button (#6521) 2023-06-20 23:31:55 +02:00
femmdi de3f6aab91
Use sentence case for settings titles (#6390) 2023-06-20 22:38:07 +02:00
peking_ling 8b7d3cabac
Fix NullPointerException when chapter does not have start time (#6520) 2023-06-20 22:31:44 +02:00
Jonathan Zopf 7b5d366536
Don't request rating by F-Droid users (#6495) 2023-06-03 16:08:29 +02:00
ebraminio d51e937e96
Use the brand new material switches for preferences (#6475) 2023-05-29 13:45:25 +02:00
ebraminio 10c70dd5f1
Make drawer's corners round (#6478) 2023-05-28 11:18:56 +02:00
ByteHamster 7d1259a39a
Fix default widget background (#6494) 2023-05-18 12:42:02 +02:00
mueller-ma 3da7fcf8f0
Rounded corners of cover in widget (#6483) 2023-05-18 10:33:02 +02:00
TacoTheDank cf4345564c Remove no longer needed Compat stuff 2023-05-14 17:02:34 -04:00
TacoTheDank 90d6095dad Centralize stream library version 2023-05-14 17:01:26 -04:00
Jonathan Zopf 194067deea
Don't show copying feedback on Android 13 (#6481) 2023-05-14 18:26:46 +02:00
ByteHamster c9d74e7942 Update app store metadata 2023-05-13 22:40:49 +02:00
peking_ling c759eed50d
Fix Glide leaking reference to activity (#6446) 2023-05-07 11:43:35 +02:00
ByteHamster b8a1c1f49a
Let filter button height grow (#6477)
Also, make it easier to see what option is selected
2023-05-07 11:28:41 +02:00
mueller-ma da16f13e8b
Make single filter button full-width (#6454) 2023-05-07 11:08:27 +02:00
ByteHamster eaae6585d6
Don't show error when download was cancelled (#6476) 2023-05-06 17:54:47 +02:00
ByteHamster 6d7bfef8a5
Download Service Rewrite (#6420) 2023-05-05 23:09:03 +02:00
ByteHamster 4c286931cd Remove string where translator broke format specifiers 2023-05-05 20:02:02 +02:00
ByteHamster f8be7d596d Merge branch 'master' into develop 2023-05-05 19:43:45 +02:00
mueller-ma 967e289f91
Add .editorconfig (#6461)
This causes the 'max line length line' in Android Studio to be at 120
chars, which is the same limit as checkstyle uses.
2023-05-05 19:25:54 +02:00
mueller-ma 446b938b3a
Correctly theme download icon in settings (#6465) 2023-05-05 19:13:36 +02:00
ByteHamster 1bc053186d
Fix file provider not being able to share all files (#6472)
The library only looks at one of the external storage devices.
If the exported log file doesn't happen to be stored on
the first device, sharing it doesn't work.
This is a known issue in the Android libraries:
https://issuetracker.google.com/issues/37125252

This commit works around it by using an undocumented element that covers
the entire file system.
2023-05-05 19:12:49 +02:00
mueller-ma aab19f3a5c
Ignore subscription filter when suggesting tags (#6453)
I have the following setup:
* A tag 'Done' with fully listened podcasts
* The subscription filter "counter greater 0"
* The subscription counter "downloaded episodes"

This way all done podcasts aren't visible in the drawer. When I want to
tag another podcast as done, 'Done' isn't auto-completed. With this
change all tags will be auto-completed.
2023-05-05 17:22:31 +02:00
ByteHamster e2bbc3ef17
Work around Android telling us zero-date when no date is present (#6450) 2023-05-05 17:19:06 +02:00
mueller-ma 5a74279ce8
Mark quick settings tile as toggleable (#6464)
This removes the arrow that is currently displayed in the tile. That arrow is only shown on tiles that open a dialog or activity.
2023-05-05 17:18:42 +02:00
ByteHamster b063f0508f Bump version to 3.0.2 2023-04-28 23:20:34 +02:00
Keunes 358e64b079
Update 'Special thanks' list in the About screen (#6441) 2023-04-27 19:53:22 +02:00
ByteHamster a877809bad
Fix queue section not loading (#6447)
Apparently some devices do not support the SQLite IIF function.
2023-04-21 22:19:20 +02:00
Keunes ca0be76fdc
Add changelog script (#6399) 2023-04-15 21:17:17 +02:00
Rob Pilling e0227f9b16
Handle a null timestamp in local/first actions (#6379) 2023-04-15 21:11:05 +02:00
mueller-ma 0bdf9d9e28
Add option to enable sleep timer based on current time (#6384) 2023-04-15 21:08:03 +02:00
ByteHamster 4cdc5d14d9 Bump version to 3.0.1 2023-04-15 18:10:44 +02:00
ByteHamster 2a169d9df6 Update translations 2023-04-15 17:55:24 +02:00
ByteHamster 8396a34670
Replace Network+Storage preferences with Downloads (#6434) 2023-04-15 17:33:21 +02:00
ByteHamster e10a8b534b
Adapt teaser image to branding refresh (#6436) 2023-04-15 16:28:11 +02:00
ByteHamster 84e1ff248f
Remove 'Statistics moved' message (#6433) 2023-04-15 15:55:42 +02:00
ByteHamster 2021e0e915
Invert monochrome icon (#6431) 2023-04-15 15:54:58 +02:00
ByteHamster 1541af0fd5
Add onPrimary color (#6432) 2023-04-15 15:53:47 +02:00
ByteHamster 8ea0d1907b
Respect 'include marked as played' on home screen (#6435) 2023-04-15 15:52:45 +02:00
ByteHamster 39d309e906
Add banner on home screen if notification permission is not granted (#6412) 2023-04-07 16:37:32 +02:00
ByteHamster da9bb8d578
Fix long-pressing subscription on home screen (#6419) 2023-04-07 14:25:51 +02:00
GitStart a828660b44
Do not switch screens when clicking "Remove podcast" (#6259) 2023-04-07 14:21:52 +02:00
ByteHamster 7ed78887c4
Ignore 'new' action during sync (#6415)
We never want to overwrite the local playback state.
2023-04-06 20:01:39 +02:00
ByteHamster a08f387c56
Support longer transient pause (#6416) 2023-04-06 20:00:46 +02:00
ByteHamster 596bdaed3f
Don't crash when Chromecast media is not loaded yet (#6417) 2023-04-06 20:00:17 +02:00
ByteHamster e9ba45e2bd
Avoid scrolling screen on refresh (#6413) 2023-04-06 17:12:16 +02:00
ByteHamster 9b989fed43
Target Android 13 (#6409) 2023-04-03 21:51:54 +02:00
mueller-ma 3e101cca2a
Make widget configurable by long-pressing (#6410) 2023-04-03 21:50:13 +02:00
ByteHamster 038847177e
When both adding and removing a feed before the next sync, remove the other action (#6404) 2023-04-02 10:37:41 +02:00
ByteHamster b706ab9776
Don't crash trying to show item at negative position (#6407)
I have no idea what code path could pass a negative number there,
but apparently there are users who experience a crash when trying
to show a negative position.
2023-04-02 10:31:15 +02:00
Taco 78f65349d5
Update AGP to 7.4.0 (#5655) 2023-04-01 23:16:53 +02:00
ByteHamster 8c9b61e599
Remove 'set lockscreen background' setting (#6385)
Users disable the setting and then wonder why other apps (like Android
Auto) do not display the cover image, even though it says so in the
setting summary.
2023-03-31 22:18:37 +02:00
ByteHamster d5321a147b
Don't try to start foreground service, Android doesn't let us anyway (#6386) 2023-03-31 22:17:49 +02:00
Andrzej Węgłowski 548f9e021e
Random enqueue location (#6403) 2023-03-31 22:16:59 +02:00
ByteHamster ee69e8c66b
Remove from queue section some time after resetting playback position (#6402) 2023-03-31 22:07:41 +02:00
Keunes 214bf974cf
Mention in statistics to warning when deleting podcast(s) (#6393) 2023-03-23 21:39:14 +01:00
ByteHamster 4f7f49e1e7
Move feed download to worker (#6375)
Feed downloads are now independent from episode downloads.
This makes it easier to use WorkManager for refreshing.
Also, it will make it easier to add different refresh intervals
in the future.
2023-03-14 21:03:45 +01:00
ByteHamster 07b59d8b32
More workarounds for devices that crash when getting a cast context (#6378) 2023-03-14 19:56:23 +01:00
ByteHamster 2c0b970044
Notify Android Auto that the queue changed (#6373) 2023-03-12 20:09:12 +01:00
ByteHamster 86c11584b5
Automatically clear old download log entries on upgrade (#6370) 2023-03-11 17:44:21 +01:00
ByteHamster 835f007b67
Make statistics filter start at 00:00 (#6371) 2023-03-11 17:44:01 +01:00
ByteHamster 870fe2be56 Bump version to 3.0.0 2023-03-06 22:32:10 +01:00
ByteHamster 3ddd7f2f80 Add changelog for version 3.0 2023-03-06 22:30:13 +01:00
ByteHamster 834426cb14 Update translations 2023-03-06 22:26:47 +01:00
ByteHamster 6b6753ad84
Hide 'reconnect' settings (#6367)
Android 12+ doesn't let us start the foreground service from an event
like "headset reconnected". Hide the corresponding settings and avoid
crashing.
2023-03-06 22:08:49 +01:00
ByteHamster 95b97b6f49
Warn when local folder is empty instead of silently ignoring (#6366) 2023-03-06 21:41:05 +01:00
ByteHamster 0b3e664057
Prompt for battery optimization (#6362) 2023-03-06 21:40:34 +01:00
ByteHamster d8d94878a2
Fix crash on Huawei devices (#6365) 2023-03-06 21:39:47 +01:00
ByteHamster 24d1a06662
Instead of specialized methods, use the global 'getEpisodes' method with a filter (#6358) 2023-03-01 20:52:23 +01:00
GitStart 581e71b306
Add option to switch a- & descending sort order for Inbox (#6266) 2023-02-28 21:41:15 +01:00
ByteHamster 5e75c968ad
Re-add setting to open drawer when pressing back (#6355) 2023-02-28 20:42:46 +01:00
ByteHamster ccea00e405
Remove deprecated media players (#6354) 2023-02-26 16:38:31 +01:00
ByteHamster ebcb5e2a7c
Hack around black theme select background color having low contrast (#6352) 2023-02-25 17:00:49 +01:00
ByteHamster 9cd59a6720
Support media resumption (#6350)
This fixes the disappearing media notification after pressing the play button.
2023-02-25 16:33:11 +01:00
ByteHamster 3e077e5653
Add cancel action to download notification (#6353) 2023-02-25 16:30:58 +01:00
ByteHamster 34553475d9
Add chapters button to video player (#6348) 2023-02-25 10:54:16 +01:00
ByteHamster 5f00294c29
More human readable player error message (#6346) 2023-02-24 22:19:30 +01:00
ByteHamster 06347a3df9
Use more clear swipe action label (#6345) 2023-02-24 22:10:02 +01:00
ByteHamster 8be62b6d0e
Always add feeds from opml, even if download fails (#6347) 2023-02-24 22:08:59 +01:00
ByteHamster 6d72d7cebf
Add mobile sync setting (#6349) 2023-02-24 22:06:12 +01:00
GitStart 658c47f7a7
Resume playback does not work when streaming after data connection drops (#6272) 2023-02-24 19:08:57 +01:00
Andrew Booze 59253db2e4
Add back skip buttons and more custom actions on Android Auto (#6050) 2023-02-24 17:10:44 +01:00
ByteHamster 7753c500db
Do not jump bottom sheet when playing (#6342)
Whenever some view calls requestLayout(), the bottom view jumps.
This happens during slide when setting the player from GONE to VISIBLE.
Also, it happens every time the position changes because the TextView
has a dynamic width. We are not actually interested in the dynamic
width and can simply keep the initial width. This avoids requestLayout()
calls every time the position is updated.
2023-02-24 16:53:14 +01:00
Keunes e20d11e130
Update 'show in main list' string (#6344) 2023-02-24 16:44:57 +01:00
ByteHamster 28844af6ff
Fix crash in iTunes loader (#6341) 2023-02-23 23:05:54 +01:00
peking_ling 240737e3ac
Fix memory leaks (#6335) 2023-02-23 21:53:56 +01:00
Erik Johnson 9fed944392
Add "New Episodes Action" preference (#6095) 2023-02-22 21:34:43 +01:00
Tony Tam 5c79bc7c45
change iTunes to Apple Podcasts (#6324) 2023-02-22 20:14:19 +01:00
GitStart 25ddd73f24
Add sort option to episodes screen (#6286) 2023-02-22 20:04:04 +01:00
femmdi 50eb1e9cf9
Update iTunes to Apple Podcasts in Google Play (#6331) 2023-02-22 19:25:17 +01:00
ByteHamster 2b22d4b697 Merge branch 'master' into develop 2023-02-20 23:19:34 +01:00
ByteHamster e58e2d0639
Force-refresh feeds that failed before (#6332)
Apparently some servers return "Not Modified", even though a broken feed
was fixed in the meantime. When refreshing all feeds, now force-refresh
the feeds that previously failed.
2023-02-20 23:16:18 +01:00
ByteHamster a5d4864776
Add retry button to download failed notifications (#6333) 2023-02-20 23:15:56 +01:00
ByteHamster 2833812238 Bump version to 3.0.0-beta6 2023-02-19 19:47:27 +01:00
ByteHamster 437f094936 Update translations 2023-02-19 19:47:09 +01:00
ByteHamster c98194f519
Remove another global callback object (#6316) 2023-02-19 11:48:48 +01:00
ByteHamster cfb9745246
Clarify "show in main list" checkbox title (#6326) 2023-02-18 14:52:38 +01:00
ByteHamster 997860fe52
Extra toggle for full black theme (#6328) 2023-02-18 14:51:55 +01:00
ByteHamster 4e1a3be122
Ensure that the context is non-null when loading the shownotes (#6327) 2023-02-18 13:08:47 +01:00
ByteHamster 5b6fe580e0
Fix Chromecast on Android 12, use styled receiver (#6321) 2023-02-17 20:13:30 +01:00
ByteHamster caf49c5da8
Update duration from feed if there is none yet (#6322) 2023-02-16 21:51:58 +01:00
Jared234 7a2f4771ec
Add sorting options to downloads screen (#6210) 2023-02-12 21:12:04 +01:00
GitStart 8248bc6bb1
Automatically switch to different screen when hiding current one from drawer (#6254) 2023-02-12 21:05:24 +01:00
ByteHamster 22e6a0c40f
Fix current chapter having the same color as the dialog background (#6315) 2023-02-12 09:44:11 +01:00
GitStart 4096aaf47e
Convert subscriptions screen to cards (#6261) 2023-02-11 19:04:14 +01:00
ByteHamster f9839aba99
Don't break tab labels into multiple lines on small devices (#6313) 2023-02-11 10:14:46 +01:00
ByteHamster f9076cc8e3
Update build instructions in README (#6310) 2023-02-08 19:33:58 +01:00
Victor Häggqvist 52ddf47e36
Fix shownotes text border overlap for long translations (#6304) 2023-02-05 15:48:54 +01:00
ByteHamster 08ee701dd7 Bump version to 3.0.0-beta5 2023-02-02 00:19:08 +01:00
ByteHamster 8819487699 Update list of supported website languages 2023-02-02 00:18:34 +01:00
ByteHamster 530165206b
Fix when playback is started from MediaSessionCompat.Callback (#6295)
In that case:
- The service does not go through onStartCommand, so it does not go to foreground state.
- The media session is already destroyed.

Now, create a new media session and definitely start foreground service when something is playing.
2023-02-02 00:06:53 +01:00
ByteHamster db5d47967a
Use nicer animation when sliding up player (#6301) 2023-02-02 00:05:02 +01:00
GitStart f9e344e215
Add long press menu to search results in subscriptions list (#6267) 2023-02-02 00:04:07 +01:00
ByteHamster 7af00f7e83 Bump version to 3.0.0-beta4 2023-01-29 16:48:21 +01:00
ByteHamster 731adeaf2c
Don't stop service between episodes (#6293) 2023-01-29 16:45:26 +01:00
ByteHamster efcb710703
Avoid icons leaking below the miniplayer (#6292) 2023-01-29 14:04:03 +01:00
ByteHamster e261514c5b
Update feed url when server returns itunes:new-feed-url (#6291) 2023-01-29 12:12:08 +01:00
ByteHamster 04a8ee5787
Reduce padding of horizontal home sections to better align them with the titles (#6290) 2023-01-28 12:56:11 +01:00
GitStart 73a6ff1f60
Remove subscribed podcasts from discover / suggestions (#6269) 2023-01-28 12:53:21 +01:00
ByteHamster 12793de604
Use rasterized icon on splash (#6287) 2023-01-28 11:43:35 +01:00
ByteHamster 78bce635c4
Send first sleep timer tick earlier (#6288) 2023-01-28 10:14:53 +01:00
ByteHamster 6e7d1f1994
Work around Android's AlertDialog pushing out buttons when content gets large (#6282) 2023-01-27 19:01:20 +01:00
ByteHamster 98107419e0
Make description of tinted theme more clear (#6283) 2023-01-27 19:00:56 +01:00
GitStart 13439e1a48
Fix playback timer flickering issue while streaming and downloading episode at the same time (#6268) 2023-01-24 19:20:58 +01:00
ByteHamster cb2cc7a357 Pull translations again to fix problem with Turkish string 2023-01-18 18:27:03 +01:00
ByteHamster bb43cd4613 Bump version to 3.0.0-beta3 2023-01-17 22:58:01 +01:00
ByteHamster 94b50b37f1 Update translations 2023-01-17 22:56:39 +01:00
ByteHamster 08fdedb236 Update contributors list 2023-01-17 22:46:36 +01:00
Keunes f995fd96df
Synchronisation label changes (#6213) 2023-01-14 14:46:19 +01:00
ByteHamster 63e9d7f696
Merge pull request #6265 from ByteHamster/fix_duplicate_name
Fix downloads when feeds with same name have items with the same name
2023-01-13 17:27:16 +01:00
ByteHamster c5b34114cd
Merge pull request #6264 from ByteHamster/onlinefeed_background
Make dark background of preview more consistent
2023-01-13 17:26:42 +01:00
ByteHamster cf057acdf7
Merge pull request #6263 from ByteHamster/screen-insets
Use exactly those insets that we mark as consumed
2023-01-13 17:26:24 +01:00
ByteHamster d7bfe89b13
Branding upgrade (#6146) 2023-01-13 17:24:51 +01:00
ByteHamster 941ebbdc2b Fix downloads when feeds with same name have items with the same name 2023-01-08 21:52:41 +01:00
ByteHamster 461dcb8c11 Make dark background of preview more consistent 2023-01-08 21:34:37 +01:00
ByteHamster ba9da3b74c Use exactly those insets that we mark as consumed 2023-01-08 21:26:49 +01:00
Vishnu Sanal T 88289d02ae
Possibility to remove a single episode from playback history (#6184) 2023-01-01 15:29:23 +01:00
Vishnu Sanal T 97889a46ed
Prevent rating dialog from showing on debug variant (#6255) 2022-12-30 17:21:20 +01:00
Patrick Demers ebfda200e0
Refresh Feed after Credentials Change (#6236) 2022-12-24 17:07:43 +01:00
ByteHamster 025944d6ab Bump version to 3.0.0-beta2 2022-12-20 21:40:28 +01:00
ByteHamster 0776f232d3
Merge pull request #6247 from ByteHamster/rewrite-audio-focus
Rework audio focus handling
2022-12-20 21:18:48 +01:00
ByteHamster 670f26bb0e Rework audio focus handling
Instead of pausing the entire service, only pause media playback without
telling the service. This has the following advantages:
- It's faster
- The position does not change (because it does not need to seek)
- We can definitely resume (because we still have a foreground service)

Especially the last point is important on Android 12, where we couldn't
restart after an interruption because the service cannot be started.
2022-12-18 21:36:40 +01:00
ByteHamster fbfd7c43ac
Merge pull request #6246 from ByteHamster/revert-loading-dummy
Revert back to showing progress bars while loading
2022-12-18 21:08:52 +01:00
ByteHamster c478b49b1e Revert back to showing progress bars while loading
Dummies are slower on some devices, even when disabling their animations.
2022-12-18 18:59:30 +01:00
ByteHamster 3acec11322
Merge pull request #6237 from ByteHamster/tinted-theme
Add support for Material You tinted theme
2022-12-18 18:17:03 +01:00
ByteHamster 80a91d9da0
Merge pull request #6240 from ByteHamster/multi-select
Multi-select using background instead of checkbox
2022-12-18 17:43:38 +01:00
ByteHamster a7e5f2f4ae
Merge pull request #6239 from ByteHamster/select-country
Rename 'country' to 'select country'
2022-12-18 17:43:01 +01:00
ByteHamster d8d6f1c72f Remove support for Android 4.4 2022-12-18 15:01:59 +01:00
ByteHamster 10ee446f4e Multi-select using background instead of checkbox 2022-12-17 11:14:45 +01:00
ByteHamster 1d251492b0 Add support for Material You tinted theme 2022-12-17 10:51:07 +01:00
ByteHamster f66e3dd661 Rename 'country' to 'select country' 2022-12-17 10:43:03 +01:00
ByteHamster aa6b7b86f8
Merge pull request #6223 from ByteHamster/material-dialogs
Migrate ListPreference to Material Design 3
2022-12-17 10:37:15 +01:00
ByteHamster 95eae1519a Disable duration text box when checkbox is off 2022-12-16 20:32:02 +01:00
ByteHamster ef97411fbb Migrate ListPreference to Material Design 3 2022-12-16 20:31:58 +01:00
ByteHamster b670cf6111
Do not animate dummy views (#6231) 2022-12-11 17:56:07 +01:00
ByteHamster effe70a412
Merge pull request #6232 from ByteHamster/remove-end-icon
Remove non-functional end icon of country selector
2022-12-11 17:42:13 +01:00
ByteHamster 690eb6af8d
Merge pull request #6230 from ByteHamster/home-tweaks2
Make layout of home sections more clean and easy
2022-12-11 16:31:16 +01:00
ByteHamster 7101ea41f0
Merge pull request #6215 from ByteHamster/fix-crash-android-9
Fix dummy list items crashing Android 9
2022-12-11 16:30:35 +01:00
ByteHamster 1c2742e123
Merge pull request #6217 from ByteHamster/nextcloud-punnycode
Convert nextcloud domains to Punycode
2022-12-11 16:30:15 +01:00
ByteHamster 39ec95c3ad
Merge pull request #6218 from ByteHamster/show-skip
Show skip button even if 'continuous playback' is disabled
2022-12-11 16:30:01 +01:00
ByteHamster ac4409bcf4 Remove non-functional end icon of country selector 2022-12-11 16:20:59 +01:00
ByteHamster b1237094b2 Make layout of home sections more clean and easy 2022-12-11 15:03:58 +01:00
ByteHamster 32ffb2d1e9 Show skip button even if 'continuous playback' is disabled
It can still be used to skip the rest of an episode and load the next
one into the notification/miniplayer. There is no reason to hide the
button and instead show no button at all.
2022-12-04 21:47:01 +01:00
ByteHamster ace0724e5d Convert nextcloud domains to Punycode 2022-12-04 21:17:25 +01:00
ByteHamster 742f6f3e8a Fix dummy list items crashing Android 9 2022-12-04 18:27:51 +01:00
ByteHamster 4513711981 Bump version to 3.0.0-beta1 2022-12-03 23:01:19 +01:00
ByteHamster 2d3740e7ad Merge branch 'master' into develop 2022-12-03 22:23:49 +01:00
ByteHamster e4b6f70339
Merge pull request #6207 from ByteHamster/multiline-home-section-titles
Support for multi-line home section titles
2022-12-03 16:41:58 +01:00
ByteHamster ea7059f688
Merge pull request #6206 from ByteHamster/hide-started-episodes
Hide started episodes from 'random episodes' screen
2022-12-03 16:41:26 +01:00
ByteHamster 94b2a4288b
Merge pull request #6208 from ByteHamster/hide-progressbar
Hide progress bar when there is no progress
2022-12-03 16:40:48 +01:00
ByteHamster 655e3c6e4e Hide progress bar when there is no progress 2022-12-03 12:42:49 +01:00
ByteHamster f5adc4e824 Support for multi-line home section titles 2022-12-03 12:18:42 +01:00
ByteHamster 46d27a86e1 Hide started episodes from 'random episodes' screen 2022-12-03 11:29:15 +01:00
ByteHamster bc3b717911
Merge pull request #6200 from ByteHamster/detailed-error-message
Show human readable error message on details dialog
2022-11-30 21:16:13 +01:00
Tong Liu bec1eaa679
Remember decision option for "Remove all from inbox" dialog (#6186) 2022-11-30 21:15:38 +01:00
Jared234 f91d536ab9
Fixed bug that causes "skip" button to be unresponsive (#6170) 2022-11-30 20:28:14 +01:00
ByteHamster 97ab1725db
Merge pull request #6204 from ByteHamster/various-tweaks
Various tweaks
2022-11-30 20:20:02 +01:00
ByteHamster 63c5e2dc72 Convert drawer settings to Material 2022-11-30 19:55:45 +01:00
ByteHamster d5e80b089b Fix search for subscriptions 2022-11-30 19:55:45 +01:00
ByteHamster 1c08543430 Highlight currently playing card 2022-11-30 19:55:43 +01:00
ByteHamster 1ee85b5bb0
Merge pull request #6199 from ByteHamster/home-pull-refresh
Add pull-to-refresh to home screen
2022-11-30 19:54:28 +01:00
ByteHamster 75a795e3d7 Show human readable error message on details dialog 2022-11-30 19:53:56 +01:00
ByteHamster 2d115a0ec5 Refresh home screen when new episodes arrive in inbox 2022-11-30 19:44:35 +01:00
ByteHamster 6e9325b549 Add swipe-to-refresh to home screen 2022-11-27 17:46:28 +01:00
ByteHamster b4026a9a82
Merge pull request #6198 from ByteHamster/refresh-home
Refresh home sections when resuming
2022-11-27 17:35:36 +01:00
ByteHamster 6c4c51994d
Switch back to non-transparent navigation bar (#6111) 2022-11-27 12:42:42 +01:00
ByteHamster 6c1bf9db05 Refresh home sections when resuming 2022-11-27 12:28:24 +01:00
ByteHamster 3973f450be
Merge pull request #6195 from ByteHamster/fix-crash-android12
Fix crash on Android 12
2022-11-26 19:55:48 +01:00
ByteHamster 96231c4ee1 Fix crash on Android 12 2022-11-26 19:25:43 +01:00
Ricardo Borges Jr d62ea313d7
Add option to edit feed URL (#6185) 2022-11-26 16:47:38 +01:00
ByteHamster 807e09ecdd
Target API 31 (#6190) 2022-11-26 16:06:02 +01:00
LukasBrilla5 d585e37e11
Add button to refresh episode chapters (#6177) 2022-11-18 20:08:48 +01:00
ByteHamster 63ba5c458f
Merge pull request #6180 from ByteHamster/preferences-module
Move preferences to a new module
2022-11-10 21:37:11 +01:00
ByteHamster 085147723e
Merge pull request #6181 from ByteHamster/hide-suggestions-fdroid
Hide iTunes suggestions by default in F-Droid version
2022-11-10 21:35:29 +01:00
ByteHamster 410b8f1539 Hide iTunes suggestions by default in F-Droid version 2022-11-06 21:43:20 +01:00
ByteHamster d8a2dd5f83 Move preferences to a new module 2022-11-06 21:21:28 +01:00
ByteHamster 0101f1244e
Merge pull request #6179 from ByteHamster/lint-recursively
Check Lint recursively from the :app module
2022-11-06 13:48:31 +01:00
ByteHamster 65d58fdd9b
Merge pull request #6178 from ByteHamster/download-service-interface-module
Move DownloadService-Interface to new module
2022-11-06 13:47:30 +01:00
ByteHamster b140d7297a Move DownloadService-Interface to new module 2022-11-06 12:28:30 +01:00
ByteHamster d61745be86 Check Lint recursively from the :app module
instead of checking every module individually. This avoids having to
re-state all disabled Lint checks in all parent modules.
2022-11-06 11:55:23 +01:00
Vishnu Sanal T e4d4c69519
Add confirmation dialog for clearing history (#6175) 2022-11-06 10:56:01 +01:00
ByteHamster be8c8cef4d
Merge pull request #6176 from ByteHamster/decouple
Decouple some classes
2022-11-06 10:54:18 +01:00
ByteHamster 11292b598c Remove dependency from other classes to DownloadService 2022-11-05 20:44:53 +01:00
ByteHamster ae3971a58f No need to have other classes depend on the entire playback service when they just need one constant 2022-11-05 13:54:33 +01:00
ByteHamster 323149642a Decouple FeedItemUtil and PlaybackStatus 2022-11-05 13:19:41 +01:00
ByteHamster d08b9e196e Decouple sync service from other classes 2022-11-05 13:04:07 +01:00
ByteHamster 546c8841db
Merge pull request #6174 from ByteHamster/modularize-glide
Move Glide config to its own module
2022-11-05 12:50:13 +01:00
ByteHamster 70a847f6ba Remove dependency from MediaButtonReceiver to PlaybackService 2022-11-03 23:16:42 +01:00
ByteHamster 5b8cee0de0 Decouple restoring Playable from Preferences and storing position
Breaks dependency cycle
2022-11-03 23:04:48 +01:00
ByteHamster cd9845ed4c Make NetworkUtils less fat
Breaks dependency cycles
2022-11-03 22:49:33 +01:00
ByteHamster 6c1ec57bc1 Break dependency cycle
ClientConfig->HttpClient->UserAgentInterceptor->ClientConfig
2022-11-03 22:49:31 +01:00
ByteHamster c1fbb53805 Move Glide to its own module 2022-11-03 22:46:56 +01:00
ByteHamster 9b06bf0dc5
Merge pull request #6173 from ByteHamster/playbackservice-cleanup
PlaybackService cleanup
2022-11-03 21:35:18 +01:00
ByteHamster 6921d7162e Apply default RequestOptions globally instead of locally 2022-11-01 12:47:52 +01:00
ByteHamster 1e336ac0f8 No need to have a virtual method that is only used in one single instance 2022-11-01 12:06:21 +01:00
ByteHamster a836745079 Close video player when switching to audio 2022-11-01 12:06:21 +01:00
ByteHamster a29041cd4d No need to define INVALID_TIME multiple times 2022-11-01 12:06:13 +01:00
ByteHamster 17f2ebd7f2 Use normal pause/skip keycodes, not custom intent 2022-11-01 11:22:03 +01:00
Vishnu Sanal T c171ab6823
Remove 'seconds' and 'hours' options from sleep timer (#6148) 2022-10-29 17:26:14 +02:00
ByteHamster fe2195f051
Merge pull request #6164 from ByteHamster/print-no-message
Don't print full stack trace when there is no vorbis comment
2022-10-29 11:55:14 +02:00
ByteHamster e260056f08
Merge pull request #6166 from ByteHamster/less-noisy-glide
Do not print stacktrace for image loading errors
2022-10-29 11:54:50 +02:00
Tong Liu a87d660698
Fix Inbox not default screen when set as such (#6168) 2022-10-28 22:36:00 +02:00
ByteHamster edcf831346 Do not print stacktrace for image loading errors 2022-10-27 22:48:53 +02:00
ByteHamster b9ded7ea3b Don't print full stack trace when there is no vorbis comment 2022-10-27 22:02:08 +02:00
ByteHamster a15d94c94c
Merge pull request #6160 from ByteHamster/fix-empty-screen-scrolled
Fix toolbar on empty screen being set to scrolled state
2022-10-27 21:50:22 +02:00
ByteHamster 892b499687
Merge pull request #6159 from ByteHamster/fix-queue-sort
Fix queue sort order not being displayed
2022-10-23 21:50:25 +02:00
ByteHamster ed53f0904b Fix toolbar on empty screen being set to scrolled state 2022-10-23 21:45:11 +02:00
ByteHamster 2e54fa6981 Fix queue sort order not being displayed 2022-10-23 20:41:48 +02:00
Lukmannudin 655b880c46
Expand filter dialog by default (#6155)
Before, it did not fully show in landscape mode
2022-10-23 12:15:57 +02:00
ByteHamster cac231a461
Merge pull request #6153 from ByteHamster/fast-document-file
Speed up local folder refresh
2022-10-23 12:10:07 +02:00
ByteHamster c7e41c31b6 If file size and name are the same, just assume that the metadata is the same as well 2022-10-21 22:01:47 +02:00
ByteHamster 9624d8ce9e Speed up chapter parsing 2022-10-21 21:46:18 +02:00
ByteHamster 25dd4902ba Make loading DocumentFiles faster 2022-10-21 21:46:15 +02:00
Lukmannudin e6613807c0
Select all when focusing time in sleep timer (#6131) 2022-10-15 21:02:35 +02:00
Simon Rusinov 4c30d8ff7f
Add auto-complete to discovery country selection (#6139) 2022-10-15 11:29:39 +02:00
ByteHamster 8ff9dd829a
Merge pull request #6147 from ByteHamster/shownotes-cleaner
Remove text colors from shownotes
2022-10-15 11:06:06 +02:00
ByteHamster 7d0b0e57ee Remove text colors from shownotes 2022-10-14 22:39:05 +02:00
Lukmannudin 5dc3699361
Remove card elevation on home screen (#6132) 2022-10-14 19:24:02 +02:00
ByteHamster 504002c48f
Merge pull request #6109 from ByteHamster/home-only-greater-zero
Tweak Queue section on home screen
2022-10-14 19:03:09 +02:00
ByteHamster 232a026651
Merge pull request #6137 from ByteHamster/fix-crash-clicking-icon
Fix crash when clicking cover icon when episode is not loaded yet
2022-10-14 18:35:00 +02:00
ByteHamster 0facf7ce6a Fix crash when clicking cover icon when episode is not loaded yet 2022-10-08 12:56:54 +02:00
ByteHamster b0b95f0a05 Reorder queue section when pressing play, not when pausing 2022-10-04 19:48:47 +02:00
ByteHamster 4014951e9c
Merge pull request #6124 from ByteHamster/fix-chapter-crash
Fix chapters dialog crashing
2022-10-03 21:17:39 +02:00
ByteHamster 2a47d3136f
Merge pull request #6125 from ByteHamster/reduce-cover-size
Make cover on player screen slightly smaller
2022-10-03 21:17:24 +02:00
ByteHamster 2add262a6d Make cover on player screen slightly smaller 2022-10-03 12:17:05 +02:00
ByteHamster e4419579d7 Fix chapters dialog crashing 2022-10-03 12:12:57 +02:00
ByteHamster 9783f5dc25
Merge pull request #6121 from terminalmage/issue6117
Use "Close" label for button to dismiss chapters dialog
2022-10-03 11:46:36 +02:00
Erik Johnson 873ffa9cef Use "Close" label for button to dismiss chapters dialog
Fixes #6117.
2022-10-02 20:01:08 -05:00
ByteHamster 0cb47ac6d5
Merge pull request #6115 from ByteHamster/share-external-files-path
Add external-files-path to share provider
2022-10-02 14:55:54 +02:00
ByteHamster 7e7e945185 Add external-files-path to share provider
Sharing crashes on some devices with
"Failed to find configured root that contains
/storage/XXXX-XXXX/Android/data/de.danoeh.antennapod/files/media/x/y.mp3"
2022-10-02 12:03:12 +02:00
ByteHamster a621551658
Merge pull request #6090 from TacoTheDank/removePreDex
Remove old preDexLibs code in build.gradle
2022-10-02 11:55:34 +02:00
ByteHamster 18e5e89d12
Merge pull request #6110 from ByteHamster/fix-drag
Initialize swipe actions before using them
2022-10-02 11:54:44 +02:00
ByteHamster f1381a9358
Merge pull request #6112 from ByteHamster/splash-navbar
Extend splash blue to navigation bar
2022-10-02 11:54:30 +02:00
Keunes 1113bd71de
Decrease margin around button in episode card (#6102) 2022-10-02 11:53:53 +02:00
ByteHamster b90bfbd4dc Extend splash blue to navigation bar 2022-09-30 19:13:48 +02:00
ByteHamster 0aa50b8d23 Initialize swipe actions before using them 2022-09-30 18:40:31 +02:00
ByteHamster b605901c52 Hide episodes from 'queue' home section if played less than 1 sec 2022-09-30 17:25:52 +02:00
TacoTheDank 07b4a237f6 Remove old preDexLibs code in build.gradle 2022-09-18 22:59:26 -04:00
ByteHamster 097a491504
Rewrite include/exclude filter dialog (#6057) 2022-09-18 22:25:06 +02:00
ByteHamster bd0f54dbf6
Merge pull request #6082 from TacoTheDank/deleteRedundantIcon
Delete redundant round app icons
2022-09-18 21:51:56 +02:00
ByteHamster 261c7982de
Merge pull request #6002 from ByteHamster/material3
Material Design 3
2022-09-18 21:42:21 +02:00
ByteHamster cbff160bd5 Extend unit tests 2022-09-18 19:12:41 +02:00
ByteHamster 37b49b1e38 Use segmented buttons for filter 2022-09-18 19:12:41 +02:00
ByteHamster 2740816bb8 Round all the things 2022-09-18 19:12:41 +02:00
ByteHamster a524b81060 Expand app below system windows 2022-09-18 19:12:41 +02:00
ByteHamster e5d2d1b6ef Migrate navigation drawer to Material3 2022-09-18 19:12:41 +02:00
ByteHamster cbfa0181f4 Migrate dialogs to Material3 2022-09-18 19:12:38 +02:00
ByteHamster 8426e32fe8
Merge pull request #6086 from ByteHamster/fix-loading-views
Fix loading views inconsistently showing cover
2022-09-18 19:07:11 +02:00
ByteHamster ac8114342c Migrate Toolbars to Material3 2022-09-18 18:57:43 +02:00
ByteHamster a12854a96b Material Design 3 Bringup 2022-09-18 18:15:14 +02:00
cliambrown a528e8adfd
Add Quick Settings tile (#6006) 2022-09-18 18:10:18 +02:00
Erik Johnson fcce8e9e0e
Implement "Downloaded and unplayed" subscription counter option (#6073) 2022-09-18 18:03:10 +02:00
ByteHamster 5baa13b53d Fix loading views inconsistently showing cover 2022-09-18 17:59:59 +02:00
ByteHamster 6940c1a3c5
Merge pull request #5990 from TacoTheDank/binding_existingStuff
Clean up some existing viewbinding stuff
2022-09-18 17:55:50 +02:00
ByteHamster c26d2d289c
Merge pull request #6081 from TacoTheDank/removeRobotium
Remove robotium-solo library
2022-09-15 21:09:48 +02:00
TacoTheDank bbea9c990c Delete redundant round app icon 2022-09-14 18:01:39 -04:00
TacoTheDank 3c0d9a6d05 Clean up some existing viewbinding stuff 2022-09-14 15:01:52 -04:00
TacoTheDank 9599281fdb Remove robotium-solo library 2022-09-14 14:45:04 -04:00
ByteHamster 6f67d6905a
Merge pull request #6055 from ByteHamster/swipe-home
Add swipe actions to home screen
2022-09-13 21:50:16 +02:00
ByteHamster ad9de4467b
Add 'default screen' setting (replaces 'back button behavior') (#6041) 2022-09-10 16:09:26 +02:00
ByteHamster 5ace16b31b
Merge pull request #6067 from ByteHamster/player-screen
Fix player screen on some screen dimensions
2022-09-10 13:58:31 +02:00
ByteHamster 927af053c5 Fix player screen on some screen dimensions 2022-09-10 13:41:02 +02:00
ByteHamster 54bf4d149f
Merge pull request #6056 from ByteHamster/multi-select-remove-inbox
Multi-select to remove from inbox
2022-09-10 12:44:57 +02:00
Erik Johnson 539d0c928d
Remove "inbox and unplayed" feed counter option (#6033) 2022-09-10 12:06:32 +02:00
ByteHamster a63948ec6d
Merge pull request #6048 from ByteHamster/loading-dummy
Use dummy items instead of loading progress bar
2022-09-10 11:58:01 +02:00
ByteHamster d8ecda1b62 Multi-select to remove from inbox 2022-09-10 11:57:03 +02:00
ByteHamster 6f3d4f277d Add swipe actions to home screen 2022-09-04 10:27:06 +02:00
ByteHamster 45e625d988
Merge pull request #6053 from ByteHamster/database-optimizations
Database optimizations
2022-09-02 19:43:22 +02:00
ByteHamster 1207660787 Make queue loading more efficient 2022-08-30 19:46:59 +02:00
ByteHamster 836e2199bc Load only favorite IDs instead of whole FeedItems 2022-08-30 19:23:11 +02:00
ByteHamster c42ed1d187 Use dummy items instead of loading progress bar 2022-08-30 18:45:41 +02:00
ByteHamster 4c88a1aa69 Merge branch 'master' into develop 2022-08-28 16:02:21 +02:00
ByteHamster 0bf6f2f1fd
Merge pull request #6045 from ByteHamster/clear-search-box
Clear search box when coming back from search
2022-08-28 12:39:14 +02:00
ByteHamster a67df09d29 Clear search box when coming back from search 2022-08-27 19:46:04 +02:00
ByteHamster 0ea69e4063
Merge pull request #6043 from ByteHamster/fix-miniplayer
Fix miniplayer sometimes showing toolbar
2022-08-27 18:35:37 +02:00
ByteHamster f6a338f1d1
Merge pull request #6044 from ByteHamster/fix-back-icon
Fix back icon when opening screens from home
2022-08-27 18:35:10 +02:00
ByteHamster 6a0f646506 Fix miniplayer sometimes showing toolbar 2022-08-27 12:33:10 +02:00
ByteHamster 58515df6ec Fix back icon when opening screens from home 2022-08-27 12:29:26 +02:00
ByteHamster 77104c9038
Home Screen (#5864)
Co-authored-by: ueen <ueli.sarnighausen@online.de>
2022-08-27 11:19:34 +02:00
ByteHamster ec92722c04
Merge pull request #6039 from ByteHamster/statistics-performance
Make statistics loading more efficient
2022-08-26 21:01:53 +02:00
ByteHamster 28a397c897 Make statistics loading more efficient 2022-08-26 20:26:28 +02:00
ByteHamster 38dcfa9d35
Merge pull request #6030 from ByteHamster/rework-smart-shuffle
Rework smart shuffle
2022-08-25 22:02:20 +02:00
Erik Johnson 732462438a
Make widget buttons fill all horizontal space (#6031) 2022-08-25 21:58:26 +02:00
ByteHamster 7042b8d616
Use atd emulator image (#6032) 2022-08-21 17:31:57 +02:00
ByteHamster a265d17f54 Rework smart shuffle 2022-08-20 21:24:11 +02:00
ByteHamster 6e199de7ab
Merge pull request #6029 from ByteHamster/subscription-icon
Change subscriptions icon from folder to grid view
2022-08-20 21:23:39 +02:00
ByteHamster 0c8c860040 Print more logs in emulator tests 2022-08-20 21:05:10 +02:00
ByteHamster d3b5b0e14e Change subscriptions icon from folder to grid view 2022-08-20 16:47:14 +02:00
Dhruv Patidar 8e994165e6
Use downwards arrow icon to close player screen (#6012) 2022-08-14 12:22:01 +02:00
1628 changed files with 72263 additions and 64381 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
# Settings in .editorconfig should match checkstyle config
root = true
[*]
charset = utf-8
max_line_length = 120

View File

@ -11,7 +11,9 @@ body:
attributes:
label: Checklist
options:
- label: I have used the search function for [open](https://github.com/AntennaPod/AntennaPod/issues) **and** [closed](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if someone else has already submitted the same bug report.
- label: I have used the search function for [**OPEN**](https://github.com/AntennaPod/AntennaPod/issues) issues to see if someone else has already submitted the same bug report.
required: true
- label: I have **also** used the search function for [**CLOSED**](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if the problem is already solved and just waiting to be released.
required: true
- label: I will describe the problem with as much detail as possible.
required: true

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: AntennaPod Forum
- name: Help & Support
url: https://forum.antennapod.org/
about: Reduce developer's workload by asking other users.
about: Reduce developer's support workload by asking other users on our forum.
- name: F-Droid Release
url: https://antennapod.org/documentation/general/f-droid
about: Waiting for an update to appear on F-Droid? No need to create an issue, please just be patient!

View File

@ -1,12 +1,15 @@
name: Feature request
description: Request a new feature or enhancement
labels: ["Needs: Triage", "Type: Feature request"]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have used the search function for [open](https://github.com/AntennaPod/AntennaPod/issues) **and** [closed](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if someone else has already submitted the same feature request.
- label: I have used the search function for [**OPEN**](https://github.com/AntennaPod/AntennaPod/issues) issues to see if someone else has already submitted the same feature request.
required: true
- label: I have **also** used the search function for [**CLOSED**](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected.
required: true
- label: I will describe the problem with as much detail as possible.
required: true

View File

@ -1 +1,15 @@
<!-- Please make sure that you have read our contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request -->
### Description
### Checklist
<!--
To help us keep the issue tracker clean and work as efficient as possible,
please make sure that you have done all of the following.
You can tick the boxes below by placing an x inside the brackets like this: [x]
-->
- [ ] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request
- [ ] I have performed a self-review of my code
- [ ] I have run the automated code checks using `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug`
- [ ] My code follows the style guidelines of the AntennaPod project: https://github.com/AntennaPod/AntennaPod/wiki/Code-style
- [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- [ ] If it is a core feature, I have added automated tests

View File

@ -2,69 +2,57 @@ name: Checks
on:
pull_request:
types: [opened, synchronize, reopened]
types: [ opened, synchronize, reopened ]
push:
branches: [master, develop]
branches: [ master, develop ]
jobs:
code-style:
name: "Code Style"
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Checkstyle
run: |
curl -s -L https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.3.1/checkstyle-10.3.1-all.jar > checkstyle.jar
find . -name "*\.java" | xargs java -Dconfig_loc=config/checkstyle -jar checkstyle.jar -c config/checkstyle/checkstyle.xml
- name: Find PR Base Commit
id: vars
run: |
git fetch origin develop
echo "::set-output name=branchBaseCommit::$(git merge-base origin/develop HEAD)"
- name: Diff-Checkstyle
run: |
curl -s -L https://github.com/yangziwen/diff-check/releases/download/0.0.7/diff-checkstyle.jar > diff-checkstyle.jar
java -Dconfig_loc=config/checkstyle -jar diff-checkstyle.jar -c config/checkstyle/checkstyle-new-code.xml --git-dir . --base-rev ${{ steps.vars.outputs.branchBaseCommit }}
- name: XML of changed files
run: |
curl -s -L https://github.com/ByteHamster/android-xml-formatter/releases/download/1.1.0/android-xml-formatter.jar > android-xml-formatter.jar
git diff --name-only ${{ steps.vars.outputs.branchBaseCommit }} --diff-filter=AM | { grep "res/layout/" || true; } | xargs java -jar android-xml-formatter.jar
test $(git diff | wc -l) -eq 0 || (echo -e "\n\n===== Found XML code style violations! See output below how to fix them. =====\n\n" && git --no-pager diff --color=always && false)
wrapper-validation:
name: "Gradle Wrapper Validation"
needs: code-style
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2
static-analysis:
name: "Static Code Analysis"
needs: code-style
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
- name: Lint
run: ./gradlew lintPlayRelease lintRelease
- name: SpotBugs
run: ./gradlew spotbugsPlayDebug spotbugsDebug 2>&1 | grep -i "spotbugs"
- name: Configure parallel build
run: echo "org.gradle.parallel=true" >> local.properties
- name: XML code style
run: |
curl -s -L https://github.com/ByteHamster/android-xml-formatter/releases/download/1.1.0/android-xml-formatter.jar > android-xml-formatter.jar
find . -wholename "*/res/layout/*.xml" | xargs java -jar android-xml-formatter.jar
test $(git diff | wc -l) -eq 0 || (echo -e "\n\n===== Found XML code style violations! See output below how to fix them. =====\n\n" && git --no-pager diff --color=always && false)
- name: Checkstyle, Lint, SpotBugs
run: ./gradlew checkstyle :app:lintPlayDebug spotbugsPlayDebug spotbugsDebug
- name: Generate readable error messages for GitHub
if: failure()
run: |
git diff --name-only | xargs -I '{}' echo "::error file={},line=1,endLine=1,title=XML Format::Run android-xml-formatter.jar on this file or view CI output to see how it should be formatted."
python .github/workflows/errorPrinter.py
unit-test:
name: "Unit Test: ${{ matrix.variant }}"
needs: code-style
needs: static-analysis
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
@ -83,14 +71,21 @@ jobs:
execute-tests: false
upload-artifact: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
- name: Configure parallel build
run: echo "org.gradle.parallel=true" >> local.properties
- name: Create temporary release keystore
run: keytool -noprompt -genkey -v -keystore "app/keystore" -alias alias -storepass password -keypass password -keyalg RSA -validity 10 -dname "CN=antennapod.org, OU=dummy, O=dummy, L=dummy, S=dummy, C=US"
- name: Build
@ -98,7 +93,7 @@ jobs:
- name: Test
if: matrix.execute-tests == true
run: ./gradlew test${{ matrix.variant }}UnitTest test${{ matrix.base-variant }}UnitTest
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: matrix.upload-artifact == true
with:
name: app-play-debug.apk
@ -106,46 +101,34 @@ jobs:
emulator-test:
name: "Emulator Test"
needs: code-style
runs-on: macOS-latest
needs: static-analysis
runs-on: ubuntu-latest
timeout-minutes: 45
env:
api-level: 30
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
- name: Cache Gradle
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
- name: Configure parallel build
run: echo "org.gradle.parallel=true" >> local.properties
- name: Build with Gradle
run: ./gradlew assemblePlayDebugAndroidTest
- name: Cache AVD
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ hashFiles('.github/workflows/*') }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.api-level }}
target: aosp_atd
channel: canary
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Android Emulator test
uses: reactivecircus/android-emulator-runner@v2
with:
@ -155,8 +138,8 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: zsh .github/workflows/runEmulatorTests.sh
- uses: actions/upload-artifact@v3
script: bash .github/workflows/runEmulatorTests.sh
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-report

View File

@ -13,15 +13,15 @@ jobs:
steps:
- uses: actions/stale@v5
with:
days-before-stale: 15
days-before-close: 15
only-labels: 'Awaiting reply'
stale-issue-label: 'Still awaiting reply'
stale-pr-label: 'Still awaiting reply'
stale-issue-message: "This issue will be closed when we don't get a reply within 15 days."
stale-pr-message: "This PR will be closed when we don't get a reply within 15 days."
labels-to-remove-when-unstale: 'Awaiting reply'
close-issue-label: "Close reason: no reply"
close-pr-label: "Close reason: no reply"
close-issue-message: "This issue was closed because we didn't get a reply for 30 days."
close-pr-message: "This PR was closed because we didn't get a reply for 30 days."
days-before-stale: 7
days-before-close: 7
only-labels: 'Needs: Reply'
stale-issue-label: 'Needs: Reply still'
stale-pr-label: 'Needs: Reply still'
stale-issue-message: "This issue will be closed when we don't get a reply within 7 days."
stale-pr-message: "This PR will be closed when we don't get a reply within 7 days."
labels-to-remove-when-unstale: 'Needs: Reply'
close-issue-label: "Close reason: No reply"
close-pr-label: "Close reason: No reply"
close-issue-message: "This issue was closed because we didn't get a reply for 14 days."
close-pr-message: "This PR was closed because we didn't get a reply for 14 days."

48
.github/workflows/errorPrinter.py vendored Normal file
View File

@ -0,0 +1,48 @@
#!/bin/python
from xml.dom import minidom
import os.path
import glob
from pathlib import Path
if os.path.isfile('app/build/reports/lint-results-playDebug.xml'):
dom = minidom.parse('app/build/reports/lint-results-playDebug.xml')
issues = dom.getElementsByTagName('issue')
for issue in issues:
locations = issue.getElementsByTagName('location')
for location in locations:
print(location.attributes['file'].value + ":" + location.attributes['line'].value + " " + issue.attributes['summary'].value)
print("::error file=" + location.attributes['file'].value
+ ",line=" + location.attributes['line'].value
+ ",endLine=" + location.attributes['line'].value
+ ",title=Lint::" + issue.attributes['summary'].value + ". " + issue.attributes['explanation'].value.replace('\n', ' '))
print()
if os.path.isfile('build/reports/checkstyle/checkstyle.xml'):
dom = minidom.parse('build/reports/checkstyle/checkstyle.xml')
files = dom.getElementsByTagName('file')
for f in files:
errors = f.getElementsByTagName('error')
for error in errors:
print(f.attributes['name'].value + ":" + error.attributes['line'].value + " " + error.attributes['message'].value)
print("::error file=" + f.attributes['name'].value
+ ",line=" + error.attributes['line'].value
+ ",endLine=" + error.attributes['line'].value
+ ",title=Checkstyle::" + error.attributes['message'].value)
print()
for filename in glob.iglob('**/build/reports/spotbugs/*.xml', recursive=True):
filenamePath = Path(filename)
dom = minidom.parse(filename)
instance = dom.getElementsByTagName('BugInstance')
for inst in instance:
lines = inst.getElementsByTagName('SourceLine')
longMessage = inst.getElementsByTagName('LongMessage')[0].firstChild.nodeValue
for line in lines:
if "primary" in line.attributes:
print(line.attributes['sourcepath'].value + ": " + longMessage)
print("::error file=" + str(filenamePath.parent.parent.parent.parent.absolute()) + "/src/main/java/" + line.attributes['sourcepath'].value
+ ",line=" + line.attributes['start'].value
+ ",endLine=" + line.attributes['end'].value
+ ",title=SpotBugs::" + longMessage.replace('\n', ' '))
print()

View File

@ -1,11 +1,10 @@
#!/bin/zsh
#!/bin/bash
set -o pipefail
runTests() {
./gradlew connectedPlayDebugAndroidTest connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.notAnnotation=de.test.antennapod.IgnoreOnCi \
| grep -v "V/InstrumentationResultParser: INSTRUMENTATION_STATUS"
-Pandroid.testInstrumentationRunnerArguments.notAnnotation=de.test.antennapod.IgnoreOnCi
}
# Retry tests to make them less flaky

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "app/src/main/play"]
path = app/src/main/play
url = https://github.com/AntennaPod/StoreMetadata.git
branch = main

View File

@ -57,6 +57,7 @@ trans.de = app/src/main/play/listings/de-DE/full-description.txt
trans.el = app/src/main/play/listings/el-GR/full-description.txt
trans.es = app/src/main/play/listings/es-ES/full-description.txt
trans.et = app/src/main/play/listings/et/full-description.txt
trans.eu = app/src/main/play/listings/eu-ES/full-description.txt
trans.fa = app/src/main/play/listings/fa/full-description.txt
trans.fi = app/src/main/play/listings/fi-FI/full-description.txt
trans.fr = app/src/main/play/listings/fr-FR/full-description.txt
@ -98,6 +99,7 @@ trans.de = app/src/main/play/listings/de-DE/short-description.txt
trans.el = app/src/main/play/listings/el-GR/short-description.txt
trans.es = app/src/main/play/listings/es-ES/short-description.txt
trans.et = app/src/main/play/listings/et/short-description.txt
trans.eu = app/src/main/play/listings/eu-ES/short-description.txt
trans.fa = app/src/main/play/listings/fa/short-description.txt
trans.fi = app/src/main/play/listings/fi-FI/short-description.txt
trans.fr = app/src/main/play/listings/fr-FR/short-description.txt

View File

@ -28,25 +28,41 @@ If you would like to translate the app into another language or improve an exist
Submit a pull request
---------------------
- If you want to work on a feature that has been requested or fix a bug that has been reported on the "issues" page, add a comment to it so that other people know that you are working on it.
- Fork the repository.
- Almost all changes of AntennaPod are done on the `develop` branch. If a new version of AntennaPod is released, the `develop` branch is merged into `master`. As a result, the `master` branch probably doesn't contain the latest changes when you are reading this. Please make sure that you are branching from `develop`! Otherwise, there might be a lot of merge-conflicts when merging your changes into `develop` and therefore it might take longer to review your pull-request. Exceptions are urgent issues that need to be fixed in the production version.
- If your pull request fixes a bug that has been reported or implements a feature that has been requested in another issue, try to mention it in the message, so that it can be closed once your pull request has been merged. If you use special keywords in the [commit comment](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) or [pull request text](https://github.blog/2013-05-14-closing-issues-via-pull-requests/), GitHub will close the issue(s) automatically.
- If possible, add unit tests for your pull request and make sure that they pass.
- Please do not upgrade dependencies or build tools unless you have a good reason for it. Doing so can easily introduce bugs that are hard to track down.
- If you plan to do a change that touches many files (10+), please ask beforehand. This usually causes merge conflicts for other developers.
- Please follow our code style. You can use Checkstyle within Android Studio using our [configuration file](https://github.com/AntennaPod/AntennaPod/blob/develop/config/checkstyle/checkstyle-new-code.xml).
- Please only change the English string resources. Translations are handled on [Transifex](https://www.transifex.com/antennapod/antennapod/).
- Before you work on the code
- Make sure that there is an issue *without* the `Needs: Triage` or `Needs: Decision` label for the feature you want to implement or bug you want to fix.
- Add a comment to the issue so that other people know that you are working on it.
- You don't need to ask for permission to work on something, just indicate that you are doing so.
- If you want to discuss the approach to take, feel free to ask in the issue or join a [community call](https://antennapod.org/events/community-meeting).
- Fork the repository
- Create a new branch for your contribution
- This makes opening possible additional pull requests easier.
- As a base, use the `develop` branch.
- Almost all changes of AntennaPod are done on the `develop` branch. If a new version of AntennaPod is released, the `develop` branch is merged into `master`. As a result, the `master` branch probably doesn't contain the latest changes when you are reading this. Otherwise, there might be a lot of merge-conflicts when merging your changes into `develop` and therefore it might take longer to review your pull-request.
- Get coding :)
- If possible, add unit tests for your pull request and make sure that they pass.
- Please do not upgrade dependencies or build tools unless you have a good reason for it. Doing so can easily introduce bugs that are hard to track down.
- Please follow our code style. You can use Checkstyle within Android Studio using our [configuration file](https://github.com/AntennaPod/AntennaPod/blob/develop/config/checkstyle/checkstyle.xml).
- To check the code style locally, run `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug`
- Please only change the English string resources. Translations are handled on [Transifex](https://www.transifex.com/antennapod/antennapod/).
- Open the PR
- Mention the corresponding issue in the pull request text, so that it can be closed once your pull request has been merged. If you use [special keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue), GitHub will close the issue(s) automatically.
Building From Source
--------------------------
1. Fork this repository
1. Download Android Studio
1. In Android Studio
1. File » New » Project from version control
2. Enter the remote url of the forked repo
2. Wait for a long time until all progress bars go away
3. Press the Play button
1. Download AntennaPod
1. Option A: Using the git command line (recommended)
1. Use `git clone <url>` with the remote url of your forked repo.
The AntennaPod repo contains a large submodule with app store metadata like screenshots.
You **do not need that** for normal development.
1. In Android Studio: File » New » Project from existing sources
1. Option B: From Android Studio
1. File » New » Project from version control
1. Enter the remote url of the forked repo
1. Wait for a long time until all progress bars go away
1. Press the Play button
Testing and Verifying
--------------------------

File diff suppressed because one or more lines are too long

View File

@ -8,14 +8,14 @@ This is the official repository of AntennaPod, the easy-to-use, flexible and ope
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="70">](https://f-droid.org/app/de.danoeh.antennapod)
<img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/00.png" alt="Screenshot 0" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/01.png" alt="Screenshot 1" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/02.png" alt="Screenshot 2" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/03.png" alt="Screenshot 3" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/04.png" alt="Screenshot 4" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/05.png" alt="Screenshot 5" height="200">
<img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/00.png" alt="Screenshot 0" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/01.png" alt="Screenshot 1" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/02.png" alt="Screenshot 2" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/03.png" alt="Screenshot 3" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/04.png" alt="Screenshot 4" height="200"> <img src="https://raw.githubusercontent.com/AntennaPod/StoreMetadata/main/listings/en-US/graphics/phone-screenshots/05.png" alt="Screenshot 5" height="200">
## Feedback
You can use the [AntennaPod Forum](https://forum.antennapod.org/) for discussions about the app or just podcasting in general.
Bug reports and feature requests can be submitted [here](https://github.com/AntennaPod/AntennaPod/issues) (please read the [instructions](https://github.com/AntennaPod/AntennaPod/blob/master/CONTRIBUTING.md) on how to report a bug and how to submit a feature request first!).
Bug reports and feature requests can be submitted [here](https://github.com/AntennaPod/AntennaPod/issues) (please read the [instructions](https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md) on how to report a bug and how to submit a feature request first!).
We also hold regular community calls to discuss anything AntennaPod-related. [Come join the next call](https://forum.antennapod.org/t/monthly-community-call/1869)!
@ -27,10 +27,11 @@ AntennaPod has many users and we don't want them to run into trouble when we add
AntennaPod is licensed under the GNU General Public License (GPL-3.0). You can find the license text in the LICENSE file.
## Translating AntennaPod
If you want to translate AntennaPod into another language, you can visit the [Transifex project page](https://www.transifex.com/antennapod/antennapod/).
## Building AntennaPod
Information on how to build AntennaPod can be found in the [wiki](https://github.com/AntennaPod/AntennaPod/wiki/Building-AntennaPod).
You can build AntennaPod just like any other Android project. Refer to the [instructions](https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md) for more details.

View File

@ -1,29 +1,19 @@
plugins {
id('com.android.application')
id('com.github.triplet.play') version '3.7.0-agp4.2' apply false
id('com.github.triplet.play') version '3.9.0' apply false
}
apply from: "../common.gradle"
apply from: "../playFlavor.gradle"
android {
namespace "de.danoeh.antennapod"
defaultConfig {
// Version code schema:
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 2070195
versionName "2.7.1"
def commit = ""
try {
def hashStdOut = new ByteArrayOutputStream()
exec {
commandLine "git", "rev-parse", "--short", "HEAD"
standardOutput = hashStdOut
}
commit = hashStdOut.toString().trim()
} catch (Exception ignore) {
}
buildConfigField "String", "COMMIT_HASH", ('"' + (commit.isEmpty() ? "Unknown commit" : commit) + '"')
versionCode 3040095
versionName "3.4.0"
javaCompileOptions {
annotationProcessorOptions {
@ -56,40 +46,51 @@ android {
}
}
lintOptions {
disable 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams',
'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'AllowBackup', 'VectorDrawableCompat',
'StaticFieldLeak', 'UseCompoundDrawables', 'NestedWeights', 'Overdraw', 'UselessParent', 'TextFields',
'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription',
lint {
disable 'CheckResult', 'MissingMediaBrowserServiceIntentFilter', 'UnusedAttribute', 'InflateParams',
'RestrictedApi', 'ExportedReceiver', 'NotifyDataSetChanged', 'UseCompoundDrawables', 'NestedWeights',
'Overdraw', 'UselessParent', 'TextFields', 'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility',
'KeyboardInaccessibleWidget', 'LabelFor', 'SetTextI18n', 'HardcodedText', 'RelativeOverlap',
'RtlCompat', 'RtlHardcoded', 'MissingMediaBrowserServiceIntentFilter', 'VectorPath',
'InvalidPeriodicWorkRequestInterval'
'RtlHardcoded', 'RtlEnabled', 'ContentDescription'
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
dexOptions {
jumboMode true
androidResources {
additionalParameters += ["--no-version-vectors"]
generateLocaleConfig true
}
}
dependencies {
implementation project(":core")
implementation project(":event")
implementation project(':model')
implementation project(':net:common')
implementation project(':net:discovery')
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':net:download:service-interface')
implementation project(':net:download:service')
implementation project(':net:ssl')
implementation project(':net:sync:service')
implementation project(':parser:feed')
implementation project(':parser:transcript')
implementation project(':playback:base')
implementation project(':playback:cast')
implementation project(':storage:database')
implementation project(':storage:importexport')
implementation project(':storage:preferences')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
implementation project(':ui:discovery')
implementation project(':ui:echo')
implementation project(':ui:episodes')
implementation project(':ui:glide')
implementation project(':ui:i18n')
implementation project(':ui:notifications')
implementation project(':ui:widget')
implementation project(':ui:preferences')
implementation project(':ui:statistics')
implementation project(':net:sync:service-interface')
implementation project(':playback:service')
implementation project(':ui:chapters')
implementation project(':ui:transcript')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
@ -98,7 +99,6 @@ dependencies {
implementation "androidx.fragment:fragment:$fragmentVersion"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.media:media:$mediaVersion"
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.palette:palette:$paletteVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
@ -110,31 +110,23 @@ dependencies {
implementation "commons-io:commons-io:$commonsioVersion"
implementation "org.jsoup:jsoup:$jsoupVersion"
implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
implementation "com.squareup.okio:okio:$okioVersion"
implementation "org.greenrobot:eventbus:$eventbusVersion"
annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion"
implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion"
implementation 'com.leinardi.android:speed-dial:3.2.0'
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation 'com.github.skydoves:balloon:1.4.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.5.0'
implementation 'com.github.skydoves:balloon:1.5.3'
implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3'
implementation 'com.annimon:stream:1.2.2'
// Non-free dependencies:
playImplementation 'com.google.android.play:core:1.8.0'
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "junit:junit:$junitVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
@ -150,13 +142,3 @@ if (project.hasProperty("antennaPodPlayPublisherCredentials")) {
serviceAccountCredentials.set(file(antennaPodPlayPublisherCredentials))
}
}
task copyLicense(type: Copy) {
from "../LICENSE"
into "src/main/assets/"
rename { String fileName ->
fileName + ".txt"
}
}
preBuild.dependsOn copyLicense

View File

@ -28,10 +28,6 @@
public *;
}
# for okhttp
-dontwarn okhttp3.**
-dontwarn okio.**
# android-iconify
-keep class com.joanzapata.** { *; }

View File

@ -1,52 +0,0 @@
package de.danoeh.antennapod.core.service.download;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import de.danoeh.antennapod.model.download.DownloadStatus;
public class StubDownloader extends Downloader {
private final long downloadTime;
@NonNull
private final Consumer<DownloadStatus> onDownloadComplete;
public StubDownloader(@NonNull DownloadRequest request, long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) {
super(request);
this.downloadTime = downloadTime;
this.onDownloadComplete = onDownloadComplete;
}
@Override
protected void download() {
try {
Thread.sleep(downloadTime);
} catch (Throwable t) {
t.printStackTrace();
}
onDownloadComplete.accept(result);
}
@NonNull
@Override
public DownloadRequest getDownloadRequest() {
return super.getDownloadRequest();
}
@NonNull
@Override
public DownloadStatus getResult() {
return super.getResult();
}
@Override
public boolean isFinished() {
return super.isFinished();
}
@Override
public void cancel() {
super.cancel();
result.setCancelled();
}
}

View File

@ -18,16 +18,14 @@ import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import android.view.View;
import de.danoeh.antennapod.playback.service.PlaybackService;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import junit.framework.AssertionFailedError;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.hamcrest.Matcher;
@ -38,6 +36,7 @@ import java.util.concurrent.TimeoutException;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
@ -46,6 +45,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
public class EspressoTestUtils {
/**
@ -145,6 +145,21 @@ public class EspressoTestUtils {
};
}
public static void waitForViewToDisappear(Matcher<? super View> matcher, long maxWaitingTimeMs) {
long endTime = System.currentTimeMillis() + maxWaitingTimeMs;
while (System.currentTimeMillis() <= endTime) {
try {
onView(allOf(matcher, isDisplayed())).check(matches(not(doesNotExist())));
Thread.sleep(100);
} catch (NoMatchingViewException ex) {
return; // view has disappeared
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
throw new RuntimeException("timeout exceeded"); // or whatever exception you want
}
/**
* Clear all app databases.
*/
@ -167,17 +182,18 @@ public class EspressoTestUtils {
.edit()
.putString(UserPreferences.PREF_UPDATE_INTERVAL, "0")
.commit();
RatingDialog.init(InstrumentationRegistry.getInstrumentation().getTargetContext());
RatingDialog.saveRated();
}
public static void setLastNavFragment(String tag) {
public static void setLaunchScreen(String tag) {
InstrumentationRegistry.getInstrumentation().getTargetContext()
.getSharedPreferences(NavDrawerFragment.PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putString(NavDrawerFragment.PREF_LAST_FRAGMENT_TAG, tag)
.commit();
PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().getTargetContext())
.edit()
.putString(UserPreferences.PREF_DEFAULT_PAGE, UserPreferences.DEFAULT_PAGE_REMEMBER)
.commit();
}
public static void clearDatabase() {
@ -220,21 +236,6 @@ public class EspressoTestUtils {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
public static void tryKillDownloadService() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
context.stopService(new Intent(context, DownloadService.class));
try {
// Android has no reliable way to stop a service instantly.
// Calling stopSelf marks allows the system to destroy the service but the actual call
// to onDestroy takes until the next GC of the system, which we can not influence.
// Try to wait for the service at least a bit.
Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> !DownloadService.isRunning);
} catch (ConditionTimeoutException e) {
e.printStackTrace();
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
public static Matcher<View> actionBarOverflow() {
return allOf(isDisplayed(), withContentDescription("More options"));
}

View File

@ -8,7 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.ui.screen.AllEpisodesFragment;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.ui.UITestUtils;
import org.hamcrest.Matcher;
@ -48,7 +48,7 @@ public class ShareDialogTest {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
EspressoTestUtils.clearPreferences();
EspressoTestUtils.clearDatabase();
EspressoTestUtils.setLastNavFragment(AllEpisodesFragment.TAG);
EspressoTestUtils.setLaunchScreen(AllEpisodesFragment.TAG);
UITestUtils uiTestUtils = new UITestUtils(context);
uiTestUtils.setup();
uiTestUtils.addLocalFeedData(true);
@ -59,7 +59,7 @@ public class ShareDialogTest {
onDrawerItem(withText(R.string.episodes_label)).perform(click());
Matcher<View> allEpisodesMatcher;
allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2));
allEpisodesMatcher = Matchers.allOf(withId(R.id.recyclerView), isDisplayed(), hasMinimumChildCount(2));
onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000));
onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click()));
onView(first(EspressoTestUtils.actionBarOverflow())).perform(click());

View File

@ -3,45 +3,39 @@ package de.test.antennapod.playback;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import android.view.KeyEvent;
import android.view.View;
import androidx.preference.PreferenceManager;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.playback.service.PlaybackController;
import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.storage.database.LongList;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.IgnoreOnCi;
import de.test.antennapod.ui.UITestUtils;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
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.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.IgnoreOnCi;
import de.test.antennapod.ui.UITestUtils;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
@ -68,31 +62,20 @@ import static org.junit.Assert.assertTrue;
*/
@LargeTest
@IgnoreOnCi
@RunWith(Parameterized.class)
public class PlaybackTest {
@Rule
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Parameterized.Parameter(value = 0)
public String playerToUse;
private UITestUtils uiTestUtils;
protected Context context;
private PlaybackController controller;
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> initParameters() {
return Arrays.asList(new Object[][] { { "exoplayer" }, { "builtin" }, { "sonic" } });
}
@Before
public void setUp() throws Exception {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
EspressoTestUtils.clearPreferences();
EspressoTestUtils.clearDatabase();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, playerToUse).apply();
uiTestUtils = new UITestUtils(context);
uiTestUtils.setup();
}
@ -241,18 +224,19 @@ public class PlaybackTest {
}
private void skipEpisode() {
IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE);
context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT));
}
protected void pauseEpisode() {
IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE);
context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE));
}
protected void startLocalPlayback() {
openNavDrawer();
onDrawerItem(withText(R.string.episodes_label)).perform(click());
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered());
final List<FeedItem> episodes = DBReader.getEpisodes(0, 10,
FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD);
Matcher<View> allEpisodesMatcher = allOf(withId(R.id.recyclerView), isDisplayed(), hasMinimumChildCount(2));
onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000));
onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton)));
@ -287,7 +271,8 @@ public class PlaybackTest {
uiTestUtils.addLocalFeedData(true);
DBWriter.clearQueue().get();
activityTestRule.launchActivity(new Intent());
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered());
final List<FeedItem> episodes = DBReader.getEpisodes(0, 10,
FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD);
startLocalPlayback();
FeedMedia media = episodes.get(0).getMedia();

View File

@ -1,214 +0,0 @@
package de.test.antennapod.service.download;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.test.antennapod.EspressoTestUtils;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.model.feed.Feed;
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.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.DownloaderFactory;
import de.danoeh.antennapod.core.service.download.StubDownloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import static de.test.antennapod.util.event.DownloadEventListener.withDownloadEventListener;
import static de.test.antennapod.util.event.FeedItemEventListener.withFeedItemEventListener;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @see HttpDownloaderTest for the test of actual download (and saving the file).
*/
@RunWith(AndroidJUnit4.class)
public class DownloadServiceTest {
private FeedMedia testMedia11 = null;
private DownloaderFactory origFactory = null;
@Before
public void setUp() throws Exception {
EspressoTestUtils.clearDatabase();
EspressoTestUtils.clearPreferences();
origFactory = DownloadService.getDownloaderFactory();
Feed testFeed = setUpTestFeeds();
testMedia11 = testFeed.getItemAtIndex(0).getMedia();
}
private Feed setUpTestFeeds() throws Exception {
// To avoid complication in case of test failures, leaving behind orphaned
// media files: add a timestamp so that each test run will have its own directory for media files.
Feed feed = new Feed("url", null, "Test Feed title 1 " + System.currentTimeMillis());
List<FeedItem> items = new ArrayList<>();
feed.setItems(items);
FeedItem item1 = new FeedItem(0, "Item 1-1", "Item 1-1", "url", new Date(), FeedItem.NEW, feed);
items.add(item1);
FeedMedia media1 = new FeedMedia(0, item1, 123, 1, 1, "audio/mp3", null, "http://example.com/episode.mp3", false, null, 0, 0);
item1.setMedia(media1);
DBWriter.setFeedItem(item1).get();
return feed;
}
@After
public void tearDown() throws Exception {
DownloadService.setDownloaderFactory(origFactory);
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
DownloadService.cancelAll(context);
context.stopService(new Intent(context, DownloadService.class));
EspressoTestUtils.tryKillDownloadService();
}
@Test
public void testEventsGeneratedCaseMediaDownloadSuccess_noEnqueue() throws Exception {
doTestEventsGeneratedCaseMediaDownloadSuccess(false, 1);
}
@Test
public void testEventsGeneratedCaseMediaDownloadSuccess_withEnqueue() throws Exception {
// enqueue itself generates additional FeedItem event
doTestEventsGeneratedCaseMediaDownloadSuccess(true, 2);
}
private void doTestEventsGeneratedCaseMediaDownloadSuccess(boolean enqueueDownloaded,
int numEventsExpected)
throws Exception {
// create a stub download that returns successful
//
// OPEN: Ideally, I'd like the download time long enough so that multiple in-progress DownloadEvents
// are generated (to simulate typical download), but it'll make download time quite long (1-2 seconds)
// to do so
DownloadService.setDownloaderFactory(new StubDownloaderFactory(50, DownloadStatus::setSuccessful));
UserPreferences.setEnqueueDownloadedEpisodes(enqueueDownloaded);
withFeedItemEventListener(feedItemEventListener -> {
try {
assertEquals(0, feedItemEventListener.getEvents().size());
assertFalse("The media in test should not yet been downloaded",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
DownloadService.download(InstrumentationRegistry.getInstrumentation().getTargetContext(), false,
DownloadRequestCreator.create(testMedia11).build());
Awaitility.await()
.atMost(5000, TimeUnit.MILLISECONDS)
.until(() -> feedItemEventListener.getEvents().size() >= numEventsExpected);
assertTrue("After media download has completed, FeedMedia object in db should indicate so.",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
assertEquals("The FeedItem should have been " + (enqueueDownloaded ? "" : "not ") + "enqueued",
enqueueDownloaded,
DBReader.getQueueIDList().contains(testMedia11.getItem().getId()));
} catch (ConditionTimeoutException cte) {
fail("The expected FeedItemEvent (for media download complete) has not been posted. "
+ cte.getMessage());
}
});
}
@Test
public void testCancelDownload_UndoEnqueue_Normal() throws Exception {
doTestCancelDownload_UndoEnqueue(false);
}
@Test
public void testCancelDownload_UndoEnqueue_AlreadyInQueue() throws Exception {
doTestCancelDownload_UndoEnqueue(true);
}
private void doTestCancelDownload_UndoEnqueue(boolean itemAlreadyInQueue) throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// let download take longer to ensure the test can cancel the download in time
DownloadService.setDownloaderFactory(
new StubDownloaderFactory(30000, DownloadStatus::setSuccessful));
UserPreferences.setEnqueueDownloadedEpisodes(true);
UserPreferences.setEnableAutodownload(false);
final long item1Id = testMedia11.getItem().getId();
if (itemAlreadyInQueue) {
// simulate item already in queue condition
DBWriter.addQueueItem(context, false, item1Id).get();
assertTrue(DBReader.getQueueIDList().contains(item1Id));
} else {
assertFalse(DBReader.getQueueIDList().contains(item1Id));
}
withFeedItemEventListener(feedItemEventListener -> {
DownloadService.download(InstrumentationRegistry.getInstrumentation().getTargetContext(), false,
DownloadRequestCreator.create(testMedia11).build());
withDownloadEventListener(downloadEventListener ->
Awaitility.await("download is actually running")
.atMost(5000, TimeUnit.MILLISECONDS)
.until(() -> downloadEventListener.getLatestEvent() != null
&& downloadEventListener.getLatestEvent().update.mediaIds.length > 0
&& downloadEventListener.getLatestEvent().update.mediaIds[0] == testMedia11.getId()));
if (itemAlreadyInQueue) {
assertEquals("download service receives the request - no event is expected before cancel is issued",
0, feedItemEventListener.getEvents().size());
} else {
Awaitility.await("item enqueue event")
.atMost(2000, TimeUnit.MILLISECONDS)
.until(() -> feedItemEventListener.getEvents().size() >= 1);
}
DownloadService.cancel(context, testMedia11.getDownload_url());
final int totalNumEventsExpected = itemAlreadyInQueue ? 1 : 3;
Awaitility.await("item dequeue event + download termination event")
.atMost(2000, TimeUnit.MILLISECONDS)
.until(() -> feedItemEventListener.getEvents().size() >= totalNumEventsExpected);
assertFalse("The download should have been canceled",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
if (itemAlreadyInQueue) {
assertTrue("The FeedItem should still be in the queue after the download is cancelled."
+ " It's there before download.",
DBReader.getQueueIDList().contains(item1Id));
} else {
assertFalse("The FeedItem should not be in the queue after the download is cancelled.",
DBReader.getQueueIDList().contains(item1Id));
}
});
}
private static class StubDownloaderFactory implements DownloaderFactory {
private final long downloadTime;
@NonNull
private final Consumer<DownloadStatus> onDownloadComplete;
StubDownloaderFactory(long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) {
this.downloadTime = downloadTime;
this.onDownloadComplete = onDownloadComplete;
}
@Nullable
@Override
public Downloader create(@NonNull DownloadRequest request) {
return new StubDownloader(request, downloadTime, onDownloadComplete);
}
}
}

View File

@ -7,12 +7,12 @@ import android.util.Log;
import java.io.File;
import java.io.IOException;
import de.danoeh.antennapod.model.feed.FeedFile;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.net.download.service.feed.remote.Downloader;
import de.danoeh.antennapod.net.download.service.feed.remote.HttpDownloader;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.model.download.DownloadRequest;
import de.danoeh.antennapod.model.download.DownloadResult;
import de.danoeh.antennapod.model.download.DownloadError;
import de.test.antennapod.util.service.download.HTTPBin;
import org.junit.After;
@ -60,32 +60,33 @@ public class HttpDownloaderTest {
urlAuth = httpServer.getBaseUrl() + "/basic-auth/user/passwd";
}
private FeedFileImpl setupFeedFile(String downloadUrl, String title, boolean deleteExisting) {
FeedFileImpl feedfile = new FeedFileImpl(downloadUrl);
private Feed setupFeedFile(String downloadUrl, String title, boolean deleteExisting) {
Feed feedfile = new Feed(downloadUrl, "");
String fileUrl = new File(destDir, title).getAbsolutePath();
File file = new File(fileUrl);
if (deleteExisting) {
Log.d(TAG, "Deleting file: " + file.delete());
}
feedfile.setFile_url(fileUrl);
feedfile.setLocalFileUrl(fileUrl);
return feedfile;
}
private Downloader download(String url, String title, boolean expectedResult) {
return download(url, title, expectedResult, true, null, null, true);
return download(url, title, expectedResult, true, null, null);
}
private Downloader download(String url, String title, boolean expectedResult, boolean deleteExisting, String username, String password, boolean deleteOnFail) {
FeedFile feedFile = setupFeedFile(url, title, deleteExisting);
DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail, null, false);
private Downloader download(String url, String title, boolean expectedResult, boolean deleteExisting,
String username, String password) {
Feed feedFile = setupFeedFile(url, title, deleteExisting);
DownloadRequest request = new DownloadRequest(feedFile.getLocalFileUrl(), url, title, 0, Feed.FEEDFILETYPE_FEED,
username, password, null, false);
Downloader downloader = new HttpDownloader(request);
downloader.call();
DownloadStatus status = downloader.getResult();
DownloadResult status = downloader.getResult();
assertNotNull(status);
assertEquals(expectedResult, status.isSuccessful());
assertTrue(status.isDone());
// the file should not exist if the download has failed and deleteExisting was true
assertTrue(!deleteExisting || new File(feedFile.getFile_url()).exists() == expectedResult);
assertTrue(!deleteExisting || new File(feedFile.getLocalFileUrl()).exists() == expectedResult);
return downloader;
}
@ -112,8 +113,9 @@ public class HttpDownloaderTest {
@Test
public void testCancel() {
final String url = httpServer.getBaseUrl() + "/delay/3";
FeedFileImpl feedFile = setupFeedFile(url, "delay", true);
final Downloader downloader = new HttpDownloader(new DownloadRequest(feedFile.getFile_url(), url, "delay", 0, feedFile.getTypeAsInt(), null, null, true, null, false));
Feed feedFile = setupFeedFile(url, "delay", true);
final Downloader downloader = new HttpDownloader(new DownloadRequest(feedFile.getLocalFileUrl(),
url, "delay", 0, Feed.FEEDFILETYPE_FEED, null, null, null, false));
Thread t = new Thread() {
@Override
public void run() {
@ -127,16 +129,13 @@ public class HttpDownloaderTest {
} catch (InterruptedException e) {
e.printStackTrace();
}
DownloadStatus result = downloader.getResult();
assertTrue(result.isDone());
DownloadResult result = downloader.getResult();
assertFalse(result.isSuccessful());
assertTrue(result.isCancelled());
assertFalse(new File(feedFile.getFile_url()).exists());
}
@Test
public void testDeleteOnFailShouldDelete() {
Downloader downloader = download(url404, "testDeleteOnFailShouldDelete", false, true, null, null, true);
Downloader downloader = download(url404, "testDeleteOnFailShouldDelete", false, true, null, null);
assertFalse(new File(downloader.getDownloadRequest().getDestination()).exists());
}
@ -146,42 +145,18 @@ public class HttpDownloaderTest {
File dest = new File(destDir, filename);
dest.delete();
assertTrue(dest.createNewFile());
Downloader downloader = download(url404, filename, false, false, null, null, false);
Downloader downloader = download(url404, filename, false, false, null, null);
assertTrue(new File(downloader.getDownloadRequest().getDestination()).exists());
}
@Test
public void testAuthenticationShouldSucceed() throws InterruptedException {
download(urlAuth, "testAuthSuccess", true, true, "user", "passwd", true);
download(urlAuth, "testAuthSuccess", true, true, "user", "passwd");
}
@Test
public void testAuthenticationShouldFail() {
Downloader downloader = download(urlAuth, "testAuthSuccess", false, true, "user", "Wrong passwd", true);
Downloader downloader = download(urlAuth, "testAuthSuccess", false, true, "user", "Wrong passwd");
assertEquals(DownloadError.ERROR_UNAUTHORIZED, downloader.getResult().getReason());
}
/* TODO: replace with smaller test file
public void testUrlWithSpaces() {
download("http://acedl.noxsolutions.com/ace/Don't Call Salman Rushdie Sneezy in Finland.mp3", "testUrlWithSpaces", true);
}
*/
private static class FeedFileImpl extends FeedFile {
public FeedFileImpl(String download_url) {
super(null, download_url, false);
}
@Override
public String getHumanReadableIdentifier() {
return download_url;
}
@Override
public int getTypeAsInt() {
return 0;
}
}
}

View File

@ -8,6 +8,7 @@ import androidx.test.filters.MediumTest;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.service.internal.LocalPSMP;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;
@ -27,7 +28,6 @@ import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.playback.LocalPSMP;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
import org.junit.After;
@ -98,7 +98,7 @@ public class PlaybackServiceMediaPlayerTest {
private void checkPSMPInfo(LocalPSMP.PSMPInfo info) {
try {
switch (info.playerStatus) {
switch (info.getPlayerStatus()) {
case PLAYING:
case PAUSED:
case PREPARED:
@ -106,11 +106,13 @@ public class PlaybackServiceMediaPlayerTest {
case INITIALIZED:
case INITIALIZING:
case SEEKING:
assertNotNull(info.playable);
assertNotNull(info.getPlayable());
break;
case STOPPED:
case ERROR:
assertNull(info.playable);
assertNull(info.getPlayable());
break;
default:
break;
}
} catch (AssertionFailedError e) {
@ -128,13 +130,16 @@ public class PlaybackServiceMediaPlayerTest {
}
private Playable writeTestPlayable(String downloadUrl, String fileUrl) {
Feed f = new Feed(0, null, "f", "l", "d", null, null, null, null, "i", null, null, "l", false);
FeedPreferences prefs = new FeedPreferences(f.getId(), false, FeedPreferences.AutoDeleteAction.NO, VolumeAdaptionSetting.OFF, null, null);
Feed f = new Feed(0, null, "f", "l", "d", null, null, null, null,
"i", null, null, "l", System.currentTimeMillis());
FeedPreferences prefs = new FeedPreferences(f.getId(), false, FeedPreferences.AutoDeleteAction.NEVER,
VolumeAdaptionSetting.OFF, FeedPreferences.NewEpisodesAction.NOTHING, null, null);
f.setPreferences(prefs);
f.setItems(new ArrayList<>());
FeedItem i = new FeedItem(0, "t", "i", "l", new Date(), FeedItem.UNPLAYED, f);
f.getItems().add(i);
FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0, 0);
FeedMedia media = new FeedMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl,
fileUrl == null ? 0 : System.currentTimeMillis(), null, 0, 0);
i.setMedia(media);
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
@ -154,15 +159,16 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
countDownLatch.countDown();
} else {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
countDownLatch.countDown();
}
} catch (AssertionFailedError e) {
@ -179,7 +185,7 @@ public class PlaybackServiceMediaPlayerTest {
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
assertFalse(psmp.isStartWhenPrepared());
callback.cancel();
psmp.shutdown();
@ -195,15 +201,16 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
countDownLatch.countDown();
} else {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
countDownLatch.countDown();
}
} catch (AssertionFailedError e) {
@ -221,7 +228,7 @@ public class PlaybackServiceMediaPlayerTest {
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
assertTrue(psmp.isStartWhenPrepared());
callback.cancel();
psmp.shutdown();
@ -237,18 +244,19 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 4) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 3) {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 1) {
assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
}
countDownLatch.countDown();
} catch (AssertionFailedError e) {
@ -264,7 +272,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().getPlayerStatus());
callback.cancel();
psmp.shutdown();
@ -280,21 +288,21 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 5) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 4) {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 3) {
assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 1) {
assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus);
assertEquals(PlayerStatus.PLAYING, newInfo.getPlayerStatus());
}
countDownLatch.countDown();
} catch (AssertionFailedError e) {
@ -310,7 +318,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().getPlayerStatus());
callback.cancel();
psmp.shutdown();
}
@ -325,15 +333,16 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
countDownLatch.countDown();
} else {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
countDownLatch.countDown();
}
} catch (AssertionFailedError e) {
@ -349,7 +358,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
assertFalse(psmp.isStartWhenPrepared());
callback.cancel();
psmp.shutdown();
@ -365,15 +374,16 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
countDownLatch.countDown();
} else {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
countDownLatch.countDown();
}
} catch (AssertionFailedError e) {
@ -389,7 +399,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.INITIALIZED, psmp.getPSMPInfo().getPlayerStatus());
assertTrue(psmp.isStartWhenPrepared());
callback.cancel();
psmp.shutdown();
@ -405,18 +415,19 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 4) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 3) {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 1) {
assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
}
countDownLatch.countDown();
} catch (AssertionFailedError e) {
@ -432,7 +443,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.PREPARED, psmp.getPSMPInfo().getPlayerStatus());
callback.cancel();
psmp.shutdown();
}
@ -447,20 +458,21 @@ public class PlaybackServiceMediaPlayerTest {
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
throw new IllegalStateException("MediaPlayer error");
}
if (countDownLatch.getCount() == 0) {
fail();
} else if (countDownLatch.getCount() == 5) {
assertEquals(PlayerStatus.INITIALIZING, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 4) {
assertEquals(PlayerStatus.INITIALIZED, newInfo.playerStatus);
assertEquals(PlayerStatus.INITIALIZED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 3) {
assertEquals(PlayerStatus.PREPARING, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARING, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 2) {
assertEquals(PlayerStatus.PREPARED, newInfo.playerStatus);
assertEquals(PlayerStatus.PREPARED, newInfo.getPlayerStatus());
} else if (countDownLatch.getCount() == 1) {
assertEquals(PlayerStatus.PLAYING, newInfo.playerStatus);
assertEquals(PlayerStatus.PLAYING, newInfo.getPlayerStatus());
}
} catch (AssertionFailedError e) {
@ -478,7 +490,7 @@ public class PlaybackServiceMediaPlayerTest {
if (assertionError != null)
throw assertionError;
assertTrue(res);
assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().playerStatus);
assertSame(PlayerStatus.PLAYING, psmp.getPSMPInfo().getPlayerStatus());
callback.cancel();
psmp.shutdown();
}
@ -492,20 +504,23 @@ public class PlaybackServiceMediaPlayerTest {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else if (initialState != PlayerStatus.PLAYING) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else {
switch (newInfo.playerStatus) {
switch (newInfo.getPlayerStatus()) {
case PAUSED:
if (latchCount == countDownLatch.getCount())
countDownLatch.countDown();
else {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
}
break;
case INITIALIZED:
@ -513,9 +528,11 @@ public class PlaybackServiceMediaPlayerTest {
countDownLatch.countDown();
} else if (countDownLatch.getCount() < latchCount) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
break;
default:
break;
}
}
@ -605,13 +622,15 @@ public class PlaybackServiceMediaPlayerTest {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
} else if (newInfo.playerStatus == PlayerStatus.PLAYING) {
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else if (newInfo.getPlayerStatus() == PlayerStatus.PLAYING) {
if (countDownLatch.getCount() == 0) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else {
countDownLatch.countDown();
}
@ -662,13 +681,15 @@ public class PlaybackServiceMediaPlayerTest {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else {
if (initialState == PlayerStatus.INITIALIZED && newInfo.playerStatus == PlayerStatus.PREPARED) {
if (initialState == PlayerStatus.INITIALIZED && newInfo.getPlayerStatus()
== PlayerStatus.PREPARED) {
countDownLatch.countDown();
} else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.playerStatus) {
} else if (initialState != PlayerStatus.INITIALIZED && initialState == newInfo.getPlayerStatus()) {
countDownLatch.countDown();
}
}
@ -691,7 +712,7 @@ public class PlaybackServiceMediaPlayerTest {
boolean res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
if (initialState != PlayerStatus.INITIALIZED) {
assertEquals(initialState, psmp.getPSMPInfo().playerStatus);
assertEquals(initialState, psmp.getPSMPInfo().getPlayerStatus());
}
if (assertionError != null)
@ -733,13 +754,15 @@ public class PlaybackServiceMediaPlayerTest {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
assertionError = new UnexpectedStateChange(newInfo.playerStatus);
if (newInfo.getPlayerStatus() == PlayerStatus.ERROR) {
if (assertionError == null) {
assertionError = new UnexpectedStateChange(newInfo.getPlayerStatus());
}
} else {
if (newInfo.playerStatus == initialState) {
if (newInfo.getPlayerStatus() == initialState) {
countDownLatch.countDown();
} else if (countDownLatch.getCount() < latchCount && newInfo.playerStatus == PlayerStatus.INITIALIZED) {
} else if (countDownLatch.getCount() < latchCount && newInfo.getPlayerStatus()
== PlayerStatus.INITIALIZED) {
countDownLatch.countDown();
}
}

View File

@ -6,9 +6,10 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import de.danoeh.antennapod.playback.service.internal.PlaybackServiceTaskManager;
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.danoeh.antennapod.ui.widget.WidgetUpdater;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.junit.After;
@ -23,7 +24,6 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
import de.danoeh.antennapod.model.playback.Playable;
import static org.junit.Assert.assertFalse;
@ -63,7 +63,8 @@ public class PlaybackServiceTaskManagerTest {
private List<FeedItem> writeTestQueue(String pref) {
final int NUM_ITEMS = 10;
Feed f = new Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url", false);
Feed f = new Feed(0, null, "title", "link", "d", null, null, null, null, "id",
null, "null", "url", System.currentTimeMillis());
f.setItems(new ArrayList<>());
for (int i = 0; i < NUM_ITEMS; i++) {
f.getItems().add(new FeedItem(0, pref + i, pref + i, "link", new Date(), FeedItem.PLAYED, f));

View File

@ -0,0 +1,22 @@
package de.test.antennapod.service.playback;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
public class SleepTimerPreferencesTest {
@Test
public void testIsInTimeRange() {
assertTrue(SleepTimerPreferences.isInTimeRange(0, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 1));
assertTrue(SleepTimerPreferences.isInTimeRange(20, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(20, 20, 8));
assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 8));
assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 6));
assertFalse(SleepTimerPreferences.isInTimeRange(20, 6, 8));
}
}

View File

@ -1,126 +0,0 @@
package de.test.antennapod.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.ui.UITestUtils;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class AutoDownloadTest {
private Context context;
private UITestUtils stubFeedsServer;
private StubDownloadAlgorithm stubDownloadAlgorithm;
@Before
public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext();
stubFeedsServer = new UITestUtils(context);
stubFeedsServer.setup();
EspressoTestUtils.clearPreferences();
EspressoTestUtils.clearDatabase();
UserPreferences.setAllowMobileStreaming(true);
// Setup: enable automatic download
// it is not needed, as the actual automatic download is stubbed.
stubDownloadAlgorithm = new StubDownloadAlgorithm();
DBTasks.setDownloadAlgorithm(stubDownloadAlgorithm);
}
@After
public void tearDown() throws Exception {
DBTasks.setDownloadAlgorithm(new AutomaticDownloadAlgorithm());
EspressoTestUtils.tryKillPlaybackService();
stubFeedsServer.tearDown();
}
/**
* A cross-functional test, ensuring playback's behavior works with Auto Download in boundary condition.
*
* Scenario:
* - For setting enqueue location AFTER_CURRENTLY_PLAYING
* - when playback of an episode is complete and the app advances to the next episode (continuous playback on)
* - when automatic download kicks in,
* - ensure the next episode is the current playing one, needed for AFTER_CURRENTLY_PLAYING enqueue location.
*/
@Test
public void downloadsEnqueuedToAfterCurrent_CurrentAdvancedToNextOnPlaybackComplete() throws Exception {
UserPreferences.setFollowQueue(true); // continuous playback
// Setup: feeds and queue
// downloads 3 of them, leave some in new state (auto-downloadable)
stubFeedsServer.addLocalFeedData(false);
List<FeedItem> queue = DBReader.getQueue();
assertTrue(queue.size() > 1);
FeedItem item0 = queue.get(0);
FeedItem item1 = queue.get(1);
// Actual test
// Play the first one in the queue
playEpisode(item0);
try {
// when playback is complete, advances to the next one, and auto download kicks in,
// ensure that currently playing has been advanced to the next one by this point.
Awaitility.await("advanced to the next episode")
.atMost(6000, MILLISECONDS) // the test mp3 media is 3-second long. twice should be enough
.until(() -> item1.getMedia().getId() == stubDownloadAlgorithm.getCurrentlyPlayingAtDownload());
} catch (ConditionTimeoutException cte) {
long actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload();
fail("when auto download is triggered, the next episode should be playing: ("
+ item1.getId() + ", " + item1.getTitle() + ") . "
+ "Actual playing: (" + actual + ")"
);
}
}
private void playEpisode(@NonNull FeedItem item) {
FeedMedia media = item.getMedia();
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
Awaitility.await("episode is playing")
.atMost(2000, MILLISECONDS)
.until(() -> item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId());
}
private static class StubDownloadAlgorithm extends AutomaticDownloadAlgorithm {
private long currentlyPlaying = -1;
@Override
public Runnable autoDownloadUndownloadedItems(Context context) {
return () -> {
if (currentlyPlaying == -1) {
currentlyPlaying = PlaybackPreferences.getCurrentlyPlayingFeedMediaId();
} else {
throw new AssertionError("Stub automatic download should be invoked once and only once");
}
};
}
long getCurrentlyPlayingAtDownload() {
return currentlyPlaying;
}
}
}

View File

@ -2,8 +2,8 @@ package de.test.antennapod.ui;
import android.content.Intent;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
@ -70,10 +70,10 @@ public class FeedSettingsTest {
clickPreference(R.string.pref_feed_skip);
onView(withText(R.string.cancel_label)).perform(click());
clickPreference(R.string.auto_delete_label);
clickPreference(R.string.pref_auto_delete_playback_title);
onView(withText(R.string.cancel_label)).perform(click());
clickPreference(R.string.feed_volume_reduction);
clickPreference(R.string.feed_volume_adapdation);
onView(withText(R.string.cancel_label)).perform(click());
}
}

View File

@ -1,16 +1,15 @@
package de.test.antennapod.ui;
import android.app.Activity;
import android.content.Intent;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.robotium.solo.Solo;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@ -19,27 +18,14 @@ import org.junit.runner.RunWith;
import java.io.IOException;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
import de.test.antennapod.EspressoTestUtils;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.ActivityResultMatchers.hasResultCode;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.test.antennapod.EspressoTestUtils.clickPreference;
import static de.test.antennapod.EspressoTestUtils.openNavDrawer;
import static de.test.antennapod.EspressoTestUtils.waitForViewGlobally;
import static org.hamcrest.Matchers.allOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* User interface tests for MainActivity.
@ -47,7 +33,6 @@ import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
private Solo solo;
private UITestUtils uiTestUtils;
@Rule
@ -62,8 +47,6 @@ public class MainActivityTest {
uiTestUtils = new UITestUtils(InstrumentationRegistry.getInstrumentation().getTargetContext());
uiTestUtils.setup();
solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityRule.getActivity());
}
@After
@ -80,7 +63,7 @@ public class MainActivityTest {
openNavDrawer();
onView(withText(R.string.add_feed_label)).perform(click());
onView(withId(R.id.addViaUrlButton)).perform(scrollTo(), click());
onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownload_url()));
onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownloadUrl()));
onView(withText(R.string.confirm_label)).perform(scrollTo(), click());
// subscribe podcast
@ -91,79 +74,4 @@ public class MainActivityTest {
// wait for podcast feed item list
waitForViewGlobally(withId(R.id.butShowSettings), 15000);
}
@Test
public void testBackButtonBehaviorGoToPage() {
openNavDrawer();
onView(withText(R.string.settings_label)).perform(click());
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_go_to_page)).perform(click());
onView(withText(R.string.subscriptions_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
onView(allOf(withId(R.id.toolbar), isDisplayed())).check(
matches(hasDescendant(withText(R.string.subscriptions_label))));
solo.goBack();
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
public void testBackButtonBehaviorOpenDrawer() {
openNavDrawer();
onView(withText(R.string.settings_label)).perform(click());
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_open_drawer)).perform(click());
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
assertTrue(((MainActivity) solo.getCurrentActivity()).isDrawerOpen());
}
@Test
public void testBackButtonBehaviorDoubleTap() {
openNavDrawer();
onView(withText(R.string.settings_label)).perform(click());
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_double_tap)).perform(click());
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
solo.goBack();
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
public void testBackButtonBehaviorPrompt() throws Exception {
openNavDrawer();
onView(withText(R.string.settings_label)).perform(click());
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_show_prompt)).perform(click());
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
onView(withText(R.string.yes)).perform(click());
Thread.sleep(100);
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
public void testBackButtonBehaviorDefault() {
openNavDrawer();
onView(withText(R.string.settings_label)).perform(click());
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_default)).perform(click());
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
}

View File

@ -7,14 +7,14 @@ import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity;
import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.screen.AllEpisodesFragment;
import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment;
import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment;
import de.danoeh.antennapod.ui.screen.queue.QueueFragment;
import de.test.antennapod.EspressoTestUtils;
import org.junit.After;
import org.junit.Before;
@ -81,12 +81,24 @@ public class NavigationDrawerTest {
UserPreferences.setHiddenDrawerItems(new ArrayList<>());
activityRule.launchActivity(new Intent());
// home
openNavDrawer();
onDrawerItem(withText(R.string.home_label)).perform(click());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.home_label)), 1000));
// queue
openNavDrawer();
onDrawerItem(withText(R.string.queue_label)).perform(click());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.queue_label)), 1000));
// Inbox
openNavDrawer();
onDrawerItem(withText(R.string.inbox_label)).perform(click());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.inbox_label)), 1000));
// episodes
openNavDrawer();
onDrawerItem(withText(R.string.episodes_label)).perform(click());
@ -143,6 +155,7 @@ public class NavigationDrawerTest {
openNavDrawer();
onDrawerItem(withText(R.string.queue_label)).perform(longClick());
onView(withText(R.string.episodes_label)).perform(click());
onView(withId(R.id.contentPanel)).perform(swipeUp());
onView(withText(R.string.playback_history_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
@ -160,8 +173,9 @@ public class NavigationDrawerTest {
openNavDrawer();
onView(first(withText(R.string.queue_label))).perform(longClick());
onView(withText(R.string.downloads_label)).perform(click());
onView(withText(R.string.queue_label)).perform(click());
onView(withId(R.id.contentPanel)).perform(swipeUp());
onView(withText(R.string.downloads_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
hidden = UserPreferences.getHiddenDrawerItems();
@ -184,7 +198,7 @@ public class NavigationDrawerTest {
onView(allOf(withText(title), isDisplayed())).perform(click());
if (i == 3) {
onView(withId(R.id.select_dialog_listview)).perform(swipeUp());
onView(withId(R.id.contentPanel)).perform(swipeUp());
}
}
@ -206,6 +220,7 @@ public class NavigationDrawerTest {
openNavDrawer();
onView(first(withText(R.string.queue_label))).perform(longClick());
onView(withId(R.id.contentPanel)).perform(swipeUp());
onView(first(withText(R.string.downloads_label))).perform(click());
onView(withText(R.string.confirm_label)).perform(click());

View File

@ -6,55 +6,40 @@ import android.content.res.Resources;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity;
import de.danoeh.antennapod.net.download.service.episode.autodownload.APCleanupAlgorithm;
import de.danoeh.antennapod.net.download.service.episode.autodownload.APNullCleanupAlgorithm;
import de.danoeh.antennapod.net.download.service.episode.autodownload.APQueueCleanupAlgorithm;
import de.danoeh.antennapod.net.download.service.episode.autodownload.EpisodeCleanupAlgorithm;
import de.danoeh.antennapod.net.download.service.episode.autodownload.EpisodeCleanupAlgorithmFactory;
import de.danoeh.antennapod.net.download.service.episode.autodownload.ExceptFavoriteCleanupAlgorithm;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation;
import de.test.antennapod.EspressoTestUtils;
import org.awaitility.Awaitility;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.test.antennapod.EspressoTestUtils;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.swipeDown;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.test.antennapod.EspressoTestUtils.clickPreference;
import static de.test.antennapod.EspressoTestUtils.waitForView;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@LargeTest
@ -80,38 +65,6 @@ public class PreferencesTest {
UserPreferences.init(activityTestRule.getActivity());
}
@Test
public void testSwitchTheme() {
final int theme = UserPreferences.getTheme();
int otherTheme;
if (theme == de.danoeh.antennapod.core.R.style.Theme_AntennaPod_Light) {
otherTheme = R.string.pref_theme_title_dark;
} else {
otherTheme = R.string.pref_theme_title_light;
}
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_set_theme_title);
onView(withText(otherTheme)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getTheme() != theme);
}
@Test
public void testSwitchThemeBack() {
final int theme = UserPreferences.getTheme();
int otherTheme;
if (theme == de.danoeh.antennapod.core.R.style.Theme_AntennaPod_Light) {
otherTheme = R.string.pref_theme_title_dark;
} else {
otherTheme = R.string.pref_theme_title_light;
}
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_set_theme_title);
onView(withText(otherTheme)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getTheme() != theme);
}
@Test
public void testEnablePersistentPlaybackControls() {
final boolean persistNotify = UserPreferences.isPersistNotify();
@ -125,38 +78,22 @@ public class PreferencesTest {
}
@Test
public void testSetLockscreenButtons() {
public void testSetNotificationButtons() {
clickPreference(R.string.user_interface_label);
String[] buttons = res.getStringArray(R.array.compact_notification_buttons_options);
clickPreference(R.string.pref_compact_notification_buttons_title);
String[] buttons = res.getStringArray(R.array.full_notification_buttons_options);
clickPreference(R.string.pref_full_notification_buttons_title);
// First uncheck checkboxes
onView(withText(buttons[0])).perform(click());
onView(withText(buttons[1])).perform(click());
// Now try to check all checkboxes
onView(withText(buttons[0])).perform(click());
onView(withText(buttons[1])).perform(click());
onView(withText(buttons[2])).perform(click());
// Make sure that the third checkbox is unchecked
onView(withText(buttons[2])).check(matches(not(isChecked())));
String snackBarText = String.format(res.getString(
R.string.pref_compact_notification_buttons_dialog_error), 2);
Awaitility.await().ignoreExceptions().atMost(4000, MILLISECONDS)
.until(() -> {
onView(withText(snackBarText)).check(doesNotExist());
return true;
});
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(UserPreferences::showRewindOnCompactNotification);
.until(() -> UserPreferences.showSkipOnFullNotification());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(UserPreferences::showFastForwardOnCompactNotification);
.until(() -> UserPreferences.showNextChapterOnFullNotification());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> !UserPreferences.showSkipOnCompactNotification());
.until(() -> !UserPreferences.showPlaybackSpeedOnFullNotification());
}
@Test
@ -165,6 +102,7 @@ public class PreferencesTest {
doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING);
doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT);
doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK);
doTestEnqueueLocation(R.string.enqueue_location_random, EnqueueLocation.RANDOM);
}
private void doTestEnqueueLocation(@StringRes int optionResId, EnqueueLocation expected) {
@ -234,16 +172,35 @@ public class PreferencesTest {
@Test
public void testAutoDelete() {
clickPreference(R.string.storage_pref);
final boolean autoDelete = UserPreferences.isAutoDelete();
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
final boolean autoDelete = UserPreferences.isAutoDelete();
onView(withText(R.string.pref_auto_delete_playback_title)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> autoDelete != UserPreferences.isAutoDelete());
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_auto_delete_playback_title)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> autoDelete == UserPreferences.isAutoDelete());
}
@Test
public void testAutoDeleteLocal() {
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_auto_delete_playback_title)).perform(click());
assertTrue(UserPreferences.isAutoDelete());
assertFalse(UserPreferences.isAutoDeleteLocal());
onView(withText(R.string.pref_auto_local_delete_title)).perform(click());
onView(withText(R.string.yes)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.isAutoDeleteLocal());
onView(withText(R.string.pref_auto_local_delete_title)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> !UserPreferences.isAutoDeleteLocal());
}
@Test
public void testPlaybackSpeeds() {
clickPreference(R.string.playback_pref);
@ -264,71 +221,13 @@ public class PreferencesTest {
.until(() -> pauseForFocusLoss == UserPreferences.shouldPauseForFocusLoss());
}
@Test
public void testDisableUpdateInterval() {
clickPreference(R.string.network_pref);
clickPreference(R.string.feed_refresh_title);
onView(withText(R.string.feed_refresh_never)).perform(click());
onView(withId(R.id.disableRadioButton)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getUpdateInterval() == 0);
}
@Test
public void testSetUpdateInterval() {
clickPreference(R.string.network_pref);
clickPreference(R.string.feed_refresh_title);
onView(withId(R.id.intervalRadioButton)).perform(click());
onView(withId(R.id.spinner)).perform(click());
int position = 1; // an arbitrary position
onData(anything()).inRoot(RootMatchers.isPlatformPopup()).atPosition(position).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getUpdateInterval() == TimeUnit.HOURS.toMillis(2));
}
@Test
public void testSetSequentialDownload() {
clickPreference(R.string.network_pref);
clickPreference(R.string.pref_parallel_downloads_title);
onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000));
onView(withClassName(endsWith("EditText"))).perform(replaceText("1"));
onView(withText(android.R.string.ok)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getParallelDownloads() == 1);
}
@Test
public void testSetParallelDownloads() {
clickPreference(R.string.network_pref);
clickPreference(R.string.pref_parallel_downloads_title);
onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000));
onView(withClassName(endsWith("EditText"))).perform(replaceText("10"));
onView(withClassName(endsWith("EditText"))).perform(closeSoftKeyboard());
onView(withText(android.R.string.ok)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getParallelDownloads() == 10);
}
@Test
public void testSetParallelDownloadsInvalidInput() {
clickPreference(R.string.network_pref);
clickPreference(R.string.pref_parallel_downloads_title);
onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000));
onView(withClassName(endsWith("EditText"))).perform(replaceText("0"));
onView(withClassName(endsWith("EditText"))).check(matches(withText("")));
onView(withClassName(endsWith("EditText"))).perform(replaceText("100"));
onView(withClassName(endsWith("EditText"))).check(matches(withText("")));
}
@Test
public void testSetEpisodeCache() {
String[] entries = res.getStringArray(R.array.episode_cache_size_entries);
String[] values = res.getStringArray(R.array.episode_cache_size_values);
String entry = entries[entries.length / 2];
final int value = Integer.parseInt(values[values.length / 2]);
clickPreference(R.string.network_pref);
clickPreference(R.string.downloads_pref);
clickPreference(R.string.pref_automatic_download_title);
clickPreference(R.string.pref_episode_cache_title);
onView(isRoot()).perform(waitForView(withText(entry), 1000));
@ -344,7 +243,7 @@ public class PreferencesTest {
String minEntry = entries[0];
final int minValue = Integer.parseInt(values[0]);
clickPreference(R.string.network_pref);
clickPreference(R.string.downloads_pref);
clickPreference(R.string.pref_automatic_download_title);
clickPreference(R.string.pref_episode_cache_title);
onView(withId(R.id.select_dialog_listview)).perform(swipeDown());
@ -359,7 +258,7 @@ public class PreferencesTest {
String[] values = res.getStringArray(R.array.episode_cache_size_values);
String maxEntry = entries[entries.length - 1];
final int maxValue = Integer.parseInt(values[values.length - 1]);
onView(withText(R.string.network_pref)).perform(click());
onView(withText(R.string.downloads_pref)).perform(click());
onView(withText(R.string.pref_automatic_download_title)).perform(click());
onView(withText(R.string.pref_episode_cache_title)).perform(click());
onView(withId(R.id.select_dialog_listview)).perform(swipeUp());
@ -371,7 +270,7 @@ public class PreferencesTest {
@Test
public void testAutomaticDownload() {
final boolean automaticDownload = UserPreferences.isEnableAutodownload();
clickPreference(R.string.network_pref);
clickPreference(R.string.downloads_pref);
clickPreference(R.string.pref_automatic_download_title);
clickPreference(R.string.pref_automatic_download_title);
Awaitility.await().atMost(1000, MILLISECONDS)
@ -392,8 +291,8 @@ public class PreferencesTest {
@Test
public void testEpisodeCleanupFavoriteOnly() {
clickPreference(R.string.network_pref);
onView(withText(R.string.pref_automatic_download_title)).perform(click());
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_episode_cleanup_title)).perform(click());
onView(withId(R.id.select_dialog_listview)).perform(swipeDown());
onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click());
@ -403,8 +302,8 @@ public class PreferencesTest {
@Test
public void testEpisodeCleanupQueueOnly() {
clickPreference(R.string.network_pref);
onView(withText(R.string.pref_automatic_download_title)).perform(click());
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_episode_cleanup_title)).perform(click());
onView(withId(R.id.select_dialog_listview)).perform(swipeDown());
onView(withText(R.string.episode_cleanup_queue_removal)).perform(click());
@ -414,8 +313,8 @@ public class PreferencesTest {
@Test
public void testEpisodeCleanupNeverAlg() {
clickPreference(R.string.network_pref);
onView(withText(R.string.pref_automatic_download_title)).perform(click());
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_episode_cleanup_title)).perform(click());
onView(withId(R.id.select_dialog_listview)).perform(swipeUp());
onView(withText(R.string.episode_cleanup_never)).perform(click());
@ -425,10 +324,9 @@ public class PreferencesTest {
@Test
public void testEpisodeCleanupClassic() {
clickPreference(R.string.network_pref);
onView(withText(R.string.pref_automatic_download_title)).perform(click());
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
onView(withText(R.string.pref_episode_cleanup_title)).perform(click());
onView(withId(R.id.select_dialog_listview)).perform(swipeUp());
onView(withText(R.string.episode_cleanup_after_listening)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> {
@ -443,8 +341,8 @@ public class PreferencesTest {
@Test
public void testEpisodeCleanupNumDays() {
clickPreference(R.string.network_pref);
clickPreference(R.string.pref_automatic_download_title);
clickPreference(R.string.downloads_pref);
onView(withText(R.string.pref_auto_delete_title)).perform(click());
clickPreference(R.string.pref_episode_cleanup_title);
String search = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, 3, 3);
onView(withText(search)).perform(scrollTo());
@ -474,7 +372,6 @@ public class PreferencesTest {
// Find next value (wrapping around to next)
int newIndex = (currentIndex + 1) % deltas.length;
onView(withText(deltas[newIndex] + " seconds")).perform(click());
onView(withText("Confirm")).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getRewindSecs() == deltas[newIndex]);
@ -496,45 +393,15 @@ public class PreferencesTest {
int newIndex = (currentIndex + 1) % deltas.length;
onView(withText(deltas[newIndex] + " seconds")).perform(click());
onView(withText("Confirm")).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getFastForwardSecs() == deltas[newIndex]);
}
}
@Test
public void testBackButtonBehaviorGoToPageSelector() {
clickPreference(R.string.user_interface_label);
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_go_to_page)).perform(click());
onView(withText(R.string.queue_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonBehavior() == UserPreferences.BackButtonBehavior.GO_TO_PAGE);
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonGoToPage().equals(QueueFragment.TAG));
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_go_to_page)).perform(click());
onView(withText(R.string.episodes_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonBehavior() == UserPreferences.BackButtonBehavior.GO_TO_PAGE);
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonGoToPage().equals(AllEpisodesFragment.TAG));
clickPreference(R.string.pref_back_button_behavior_title);
onView(withText(R.string.back_button_go_to_page)).perform(click());
onView(withText(R.string.subscriptions_label)).perform(click());
onView(withText(R.string.confirm_label)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonBehavior() == UserPreferences.BackButtonBehavior.GO_TO_PAGE);
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getBackButtonGoToPage().equals(SubscriptionFragment.TAG));
}
@Test
public void testDeleteRemovesFromQueue() {
clickPreference(R.string.storage_pref);
clickPreference(R.string.downloads_pref);
if (!UserPreferences.shouldDeleteRemoveFromQueue()) {
clickPreference(R.string.pref_delete_removes_from_queue_title);
Awaitility.await().atMost(1000, MILLISECONDS)

View File

@ -5,7 +5,7 @@ import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.ui.screen.queue.QueueFragment;
import de.test.antennapod.EspressoTestUtils;
import org.junit.Before;
import org.junit.Rule;
@ -33,7 +33,7 @@ public class QueueFragmentTest {
public void setUp() {
EspressoTestUtils.clearPreferences();
EspressoTestUtils.clearDatabase();
EspressoTestUtils.setLastNavFragment(QueueFragment.TAG);
EspressoTestUtils.setLaunchScreen(QueueFragment.TAG);
activityRule.launchActivity(new Intent());
}

View File

@ -27,7 +27,6 @@ import static de.test.antennapod.EspressoTestUtils.onDrawerItem;
import static de.test.antennapod.EspressoTestUtils.openNavDrawer;
import static de.test.antennapod.EspressoTestUtils.waitForView;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.not;
/**
* Test UI for feeds that do not have media files
@ -66,8 +65,8 @@ public class TextOnlyFeedsTest {
onDrawerItem(withText(feed.getTitle())).perform(click());
onView(withText(feed.getItemAtIndex(0).getTitle())).perform(click());
onView(isRoot()).perform(waitForView(withText(R.string.mark_read_no_media_label), 3000));
onView(withText(R.string.mark_read_no_media_label)).perform(click());
onView(isRoot()).perform(waitForView(allOf(withText(R.string.mark_read_no_media_label), not(isDisplayed())), 3000));
onView(allOf(withText(R.string.mark_read_no_media_label), isDisplayed())).perform(click());
EspressoTestUtils.waitForViewToDisappear(withText(R.string.mark_read_no_media_label), 3000);
}
}

View File

@ -78,7 +78,7 @@ public class UITestUtils {
}
}
private String hostFeed(Feed feed) throws IOException {
public String hostFeed(Feed feed) throws IOException {
File feedFile = new File(hostedFeedDir, feed.getTitle());
FileOutputStream out = new FileOutputStream(feedFile);
Rss2Generator generator = new Rss2Generator();
@ -123,22 +123,23 @@ public class UITestUtils {
for (int i = 0; i < NUM_FEEDS; i++) {
Feed feed = new Feed(0, null, "Title " + i, "http://example.com/" + i, "Description of feed " + i,
"http://example.com/pay/feed" + i, "author " + i, "en", Feed.TYPE_RSS2, "feed" + i, null, null,
"http://example.com/feed/src/" + i, false);
"http://example.com/feed/src/" + i, System.currentTimeMillis());
// create items
List<FeedItem> items = new ArrayList<>();
for (int j = 0; j < NUM_ITEMS_PER_FEED; j++) {
FeedItem item = new FeedItem(j, "Feed " + (i+1) + ": Item " + (j+1), "item" + j,
FeedItem item = new FeedItem(0, "Feed " + (i+1) + ": Item " + (j+1), "item" + j,
"http://example.com/feed" + i + "/item/" + j, new Date(), FeedItem.UNPLAYED, feed);
items.add(item);
if (!hostTextOnlyFeeds) {
File mediaFile = newMediaFile("feed-" + i + "-episode-" + j + ".mp3");
item.setMedia(new FeedMedia(j, item, 0, 0, mediaFile.length(), "audio/mp3", null, hostFile(mediaFile), false, null, 0, 0));
item.setMedia(new FeedMedia(j, item, 0, 0, mediaFile.length(), "audio/mp3",
null, hostFile(mediaFile), 0, null, 0, 0));
}
}
feed.setItems(items);
feed.setDownload_url(hostFeed(feed));
feed.setDownloadUrl(hostFeed(feed));
hostedFeeds.add(feed);
}
feedDataHosted = true;
@ -169,14 +170,13 @@ public class UITestUtils {
List<FeedItem> queue = new ArrayList<>();
for (Feed feed : hostedFeeds) {
feed.setDownloaded(true);
if (downloadEpisodes) {
for (FeedItem item : feed.getItems()) {
if (item.hasMedia()) {
FeedMedia media = item.getMedia();
int fileId = Integer.parseInt(StringUtils.substringAfter(media.getDownload_url(), "files/"));
media.setFile_url(server.accessFile(fileId).getAbsolutePath());
media.setDownloaded(true);
int fileId = Integer.parseInt(StringUtils.substringAfter(media.getDownloadUrl(), "files/"));
media.setLocalFileUrl(server.accessFile(fileId).getAbsolutePath());
media.setDownloaded(true, System.currentTimeMillis());
}
}
}

View File

@ -45,10 +45,10 @@ public class UITestUtilsTest {
assertFalse(feeds.isEmpty());
for (Feed feed : feeds) {
testUrlReachable(feed.getDownload_url());
testUrlReachable(feed.getDownloadUrl());
for (FeedItem item : feed.getItems()) {
if (item.hasMedia()) {
testUrlReachable(item.getMedia().getDownload_url());
testUrlReachable(item.getMedia().getDownloadUrl());
}
}
}
@ -77,8 +77,8 @@ public class UITestUtilsTest {
assertTrue(item.getMedia().getId() != 0);
if (downloadEpisodes) {
assertTrue(item.getMedia().isDownloaded());
assertNotNull(item.getMedia().getFile_url());
File file = new File(item.getMedia().getFile_url());
assertNotNull(item.getMedia().getLocalFileUrl());
File file = new File(item.getMedia().getLocalFileUrl());
assertTrue(file.exists());
}
}

View File

@ -1,45 +0,0 @@
package de.test.antennapod.util.event;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.event.DownloadEvent;
import io.reactivex.functions.Consumer;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.List;
/**
* Test helper to listen to {@link DownloadEvent} and handle them accordingly.
*/
public class DownloadEventListener {
private final List<DownloadEvent> events = new ArrayList<>();
/**
* Provides an listener subscribing to {@link DownloadEvent} that the callers can use.
* Note: it uses RxJava's version of {@link Consumer} because it allows exceptions to be thrown.
*/
public static void withDownloadEventListener(@NonNull Consumer<DownloadEventListener> consumer) throws Exception {
DownloadEventListener feedItemEventListener = new DownloadEventListener();
try {
EventBus.getDefault().register(feedItemEventListener);
consumer.accept(feedItemEventListener);
} finally {
EventBus.getDefault().unregister(feedItemEventListener);
}
}
@Subscribe
public void onEvent(DownloadEvent event) {
events.add(event);
}
@Nullable
public DownloadEvent getLatestEvent() {
if (events.size() == 0) {
return null;
}
return events.get(events.size() - 1);
}
}

View File

@ -1,46 +0,0 @@
package de.test.antennapod.util.event;
import androidx.annotation.NonNull;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.event.FeedItemEvent;
import io.reactivex.functions.Consumer;
/**
* Test helpers to listen {@link FeedItemEvent} and handle them accordingly
*
*/
public class FeedItemEventListener {
private final List<FeedItemEvent> events = new ArrayList<>();
/**
* Provides an listener subscribing to {@link FeedItemEvent} that the callers can use
*
* Note: it uses RxJava's version of {@link Consumer} because it allows exceptions to be thrown.
*/
public static void withFeedItemEventListener(@NonNull Consumer<FeedItemEventListener> consumer)
throws Exception {
FeedItemEventListener feedItemEventListener = new FeedItemEventListener();
try {
EventBus.getDefault().register(feedItemEventListener);
consumer.accept(feedItemEventListener);
} finally {
EventBus.getDefault().unregister(feedItemEventListener);
}
}
@Subscribe
public void onEvent(FeedItemEvent event) {
events.add(event);
}
@NonNull
public List<? extends FeedItemEvent> getEvents() {
return events;
}
}

View File

@ -7,9 +7,7 @@ import java.io.IOException;
/**
* Utility methods for FeedGenerator
*/
class GeneratorUtil {
private GeneratorUtil(){}
abstract class GeneratorUtil {
public static void addPaymentLink(XmlSerializer xml, String paymentLink, boolean withNamespace) throws IOException {
String ns = (withNamespace) ? "http://www.w3.org/2005/Atom" : null;
xml.startTag(ns, "link");

View File

@ -6,13 +6,15 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedFunding;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.parser.feed.namespace.PodcastIndex;
import de.danoeh.antennapod.core.util.DateFormatter;
/**
* Creates RSS 2.0 feeds. See FeedGenerator for more information.
@ -98,7 +100,7 @@ public class Rss2Generator implements FeedGenerator {
}
if (item.getPubDate() != null) {
xml.startTag(null, "pubDate");
xml.text(DateFormatter.formatRfc822Date(item.getPubDate()));
xml.text(formatRfc822Date(item.getPubDate()));
xml.endTag(null, "pubDate");
}
if ((flags & FEATURE_WRITE_GUID) != 0) {
@ -108,9 +110,9 @@ public class Rss2Generator implements FeedGenerator {
}
if (item.getMedia() != null) {
xml.startTag(null, "enclosure");
xml.attribute(null, "url", item.getMedia().getDownload_url());
xml.attribute(null, "url", item.getMedia().getDownloadUrl());
xml.attribute(null, "length", String.valueOf(item.getMedia().getSize()));
xml.attribute(null, "type", item.getMedia().getMime_type());
xml.attribute(null, "type", item.getMedia().getMimeType());
xml.endTag(null, "enclosure");
}
if (fundingList != null) {
@ -132,4 +134,9 @@ public class Rss2Generator implements FeedGenerator {
xml.endDocument();
}
private static String formatRfc822Date(Date date) {
SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
return format.format(date);
}
}

View File

@ -1,113 +0,0 @@
package de.danoeh.antennapod.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.util.IntentUtils;
public class RatingDialog {
private RatingDialog(){}
private static final String TAG = RatingDialog.class.getSimpleName();
private static final int AFTER_DAYS = 7;
private static WeakReference<Context> mContext;
private static SharedPreferences mPreferences;
private static Dialog mDialog;
private static final String PREFS_NAME = "RatingPrefs";
private static final String KEY_RATED = "KEY_WAS_RATED";
private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE";
public static void init(Context context) {
mContext = new WeakReference<>(context);
mPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, 0);
if (firstDate == 0) {
resetStartDate();
}
}
public static void check() {
if (mDialog != null && mDialog.isShowing()) {
return;
}
if (shouldShow()) {
try {
mDialog = createDialog();
if (mDialog != null) {
mDialog.show();
}
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
}
}
private static void rateNow() {
Context context = mContext.get();
if (context == null) {
return;
}
IntentUtils.openInBrowser(context, "https://play.google.com/store/apps/details?id=de.danoeh.antennapod");
saveRated();
}
private static boolean rated() {
return mPreferences.getBoolean(KEY_RATED, false);
}
@VisibleForTesting
public static void saveRated() {
mPreferences
.edit()
.putBoolean(KEY_RATED, true)
.apply();
}
private static void resetStartDate() {
mPreferences
.edit()
.putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
.apply();
}
private static boolean shouldShow() {
if (rated()) {
return false;
}
long now = System.currentTimeMillis();
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, now);
long diff = now - firstDate;
long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return diffDays >= AFTER_DAYS;
}
@Nullable
private static AlertDialog createDialog() {
Context context = mContext.get();
if (context == null) {
return null;
}
return new AlertDialog.Builder(context)
.setTitle(R.string.rating_title)
.setMessage(R.string.rating_message)
.setPositiveButton(R.string.rating_now_label, (dialog, which) -> rateNow())
.setNegativeButton(R.string.rating_never_label, (dialog, which) -> saveRated())
.setNeutralButton(R.string.rating_later_label, (dialog, which) -> resetStartDate())
.setOnCancelListener(dialog1 -> resetStartDate())
.create();
}
}

View File

@ -1,15 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.danoeh.antennapod"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.VIBRATE" />
<supports-screens
android:anyDensity="true"
@ -25,15 +28,12 @@
android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:name="de.danoeh.antennapod.PodcastApp"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:backupAgent=".core.backup.OpmlBackupAgent"
android:backupAgent=".storage.importexport.OpmlBackupAgent"
android:restoreAnyVersion="true"
android:theme="@style/Theme.AntennaPod.Splash"
android:usesCleartextTraffic="true"
@ -44,7 +44,7 @@
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".activity.PlaybackSpeedDialogActivity"
android:name=".ui.screen.playback.PlaybackSpeedDialogActivity"
android:noHistory="true"
android:exported="false"
android:excludeFromRecents="true"
@ -76,7 +76,6 @@
<activity
android:name=".activity.SplashActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true">
<intent-filter>
@ -99,7 +98,6 @@
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation"
android:windowSoftInputMode="stateAlwaysHidden"
android:launchMode="singleTask"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -111,53 +109,38 @@
android:host="antennapod.org"
android:pathPrefix="/deeplink/main"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="antennapod.org"
android:pathPrefix="/deeplink/search"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="de.danoeh.antennapod.intents.MAIN_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="de.danoeh.antennapod.intents.MAIN_ACTIVITY" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
</intent-filter>
</activity>
<activity
android:name=".activity.DownloadAuthenticationActivity"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:launchMode="singleInstance"/>
<activity
android:name=".activity.PreferenceActivity"
android:name=".ui.screen.preferences.PreferenceActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false"
android:label="@string/settings_label">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.MainActivity"/>
</activity>
<activity
android:name=".activity.WidgetConfigActivity"
android:label="@string/widget_settings"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGUR"/>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter>
</activity>
<receiver
android:name=".core.receiver.PlayerWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/>
<action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/player_widget_info"/>
</receiver>
<activity
android:name=".activity.OpmlImportActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
@ -180,20 +163,19 @@
<data android:scheme="https"/>
<data android:host="*"/>
<data android:pathPattern=".*.xml" />
<data android:pathPattern=".*.opml" />
<data android:pathPattern="/.*.opml" />
</intent-filter>
</activity>
<activity
android:name=".activity.BugReportActivity"
android:name=".ui.screen.preferences.BugReportActivity"
android:label="@string/bug_report_title">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
android:value="de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity"/>
</activity>
<activity
android:name=".activity.VideoplayerActivity"
android:name=".ui.screen.playback.video.VideoplayerActivity"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:supportsPictureInPicture="true"
android:screenOrientation="sensorLandscape"
@ -208,7 +190,7 @@
</activity>
<activity
android:name=".activity.OnlineFeedViewActivity"
android:name=".ui.screen.onlinefeedview.OnlineFeedViewActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:label="@string/add_feed_label"
@ -227,9 +209,9 @@
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.xml"/>
<data android:pathPattern=".*\\.rss"/>
<data android:pathPattern=".*\\.atom"/>
<data android:pathPattern="/.*\\.xml"/>
<data android:pathPattern="/.*\\.rss"/>
<data android:pathPattern="/.*\\.atom"/>
</intent-filter>
<!-- Feedburner URLs -->
@ -282,7 +264,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:pathPattern=".*\\..*/.*" />
<data android:pathPattern="/.*\\..*/.*" />
<data android:host="subscribeonandroid.com" />
<data android:host="www.subscribeonandroid.com" />
<data android:host="*subscribeonandroid.com" />
@ -323,11 +305,15 @@
<data android:mimeType="text/plain"/>
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="de.danoeh.antennapod.intents.ONLINE_FEEDVIEW" />
</intent-filter>
</activity>
<activity android:name=".activity.SelectSubscriptionActivity"
android:label="@string/shortcut_subscription_label"
android:icon="@drawable/ic_folder_shortcut"
android:icon="@drawable/ic_shortcut_subscriptions"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:exported="true">
<intent-filter>
@ -336,24 +322,7 @@
</activity>
<receiver
android:name=".receiver.ConnectivityActionReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
<receiver
android:name=".receiver.PowerConnectionReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SPAReceiver"
android:name=".spa.SPAReceiver"
android:exported="true">
<intent-filter>
<action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/>
@ -371,6 +340,7 @@
</provider>
<meta-data
tools:ignore="Deprecated"
android:name="com.google.android.actions"
android:resource="@xml/actions" />
</application>

View File

@ -1,4 +0,0 @@
# this file is generated automatically
about.html
LICENSE.txt
CONTRIBUTORS.txt

View File

@ -1,18 +0,0 @@
Copyright 2015 Joan Zapata
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
It uses FontAwesome font, licensed under OFL 1.1, which is compatible
with this library's license.
http://scripts.sil.org/cms/scripts/render_download.php?format=file&media_id=OFL_plaintext&filename=OFL.txt

View File

@ -1,13 +0,0 @@
Copyright (C) 2016 Shota Saito
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,208 +0,0 @@
ByteHamster;5811634;Maintainer
danieloeh;968613;Original creator of AntennaPod (retired)
mfietz;6860662;Maintainer (retired)
TomHennen;5216560;Maintainer (retired)
orionlee;250644;Contributor
domingos86;9538859;Contributor
TacoTheDank;32376686;Contributor
tonytamsf;149837;Contributor
damoasda;46045854;Contributor
andersonvom;69922;Contributor
shortspider;5712543;Contributor
spacecowboy;223655;Contributor
ebraminio;833473;Contributor
asdoi;36813904;Contributor
patheticpat;16046;Contributor
brad;1614;Contributor
Cj-Malone;10121513;Contributor
maxbechtold;9162198;Contributor
keunes;11229646;Maintainer
gaul;848247;Contributor
qkolj;6667105;Contributor
pachecosf;46357909;Contributor
gerardolgvr;20119298;Contributor
johnjohndoe;144518;Contributor
hannesa2;3314607;Contributor
bws9000;262625;Contributor
ahangarha;11241315;Contributor
rharriso;570910;Contributor
xgouchet;818706;Contributor
peakvalleytech;65185819;Contributor
sevenmaster;12869538;Contributor
TheRealFalcon;153674;Contributor
Slinger;75751;Contributor
vbh;56578479;Contributor
jas14;569991;Contributor
udif;809640;Contributor
malockin;12814657;Contributor
jonasburian;15125616;Contributor
dirkmueller;1029152;Contributor
jatinkumarg;20503830;Contributor
peschmae0;4450993;Contributor
orelogo;15976578;Contributor
txtd;7108931;Contributor
ydinath;4193331;Contributor
CedricCabessa;365097;Contributor
mchelen;30691;Contributor
dethstar;1239177;Contributor
drabux;10663142;Contributor
saqura;1935380;Contributor
ueen;5067479;Contributor
binarytoto;75904760;Contributor
bibz;5141956;Contributor
hzulla;1705654;Contributor
deandreamatias;21011641;Contributor
MeirAtIMDDE;4421079;Contributor
cketti;218061;Contributor
egsavage;126165;Contributor
ligi;111600;Contributor
Xeitor;8825715;Contributor
jhenninger;197274;Contributor
dreiss;4121;Contributor
liesen;26872;Contributor
nereocystis;2257107;Contributor
rezanejati;16049370;Contributor
thrillfall;15801468;Contributor
twiceyuan;2619800;Contributor
JessieVela;33134794;Contributor
HaBaLeS;730902;Contributor
volhol;11587858;Contributor
michaelmwhite;28901334;Contributor
CameronBanga;611354;Contributor
HrBDev;25826502;Contributor
HolgerJeromin;2410353;Contributor
xisberto;1914956;Contributor
jmue;898577;Contributor
katrinleinweber;9948149;Contributor
LatinSuD;451487;Contributor
24hours;650407;Contributor
SosoTughushi;19908097;Contributor
Thom-Merrilin;76849828;Contributor
fabolhak;20029691;Contributor
archibishop;36948493;Contributor
alifeflow;24603829;Contributor
avirajrsingh;69088913;Contributor
toggles;14695;Contributor
connectety;26038710;Contributor
matdb;48329535;Contributor
damlayildiz;56313500;Contributor
kingargyle;177042;Contributor
dsmith47;14109426;Contributor
FarzanKh;14272565;Contributor
hannesaa2;18496079;Contributor
myslok;2098329;Contributor
jhunnius;9149031;Contributor
a1291762;327162;Contributor
ShadowIce;59123;Contributor
Niffler;8172446;Contributor
raghulj;57007;Contributor
raghulrm;5362986;Contributor
mamehacker;16738348;Contributor
skitt;2128935;Contributor
wseemann;2296196;Contributor
datavizard;44409076;Contributor
markamaze;17114678;Contributor
mohitshah3111999;42018918;Contributor
moralesg;14352147;Contributor
mr-intj;6268767;Contributor
tamizh143;50977879;Contributor
tuxayo;2678215;Contributor
alimemonzx;44647595;Contributor
dev-darrell;52300159;Contributor
jmdouglas;10855634;Contributor
olivoto;15932680;Contributor
PtilopsisLeucotis;54054883;Contributor
abhinavg1997;60095795;Contributor
adrns;13379985;Contributor
alanorth;191754;Contributor
alexte;7724992;Contributor
andrey-krutov;1488973;Contributor
arantius;84729;Contributor
BoJacobs;25435640;Contributor
chetan882777;36985543;Contributor
chrissicool;232590;Contributor
britiger;2057760;Contributor
cszucko;1810383;Contributor
CWftw;1498303;Contributor
danielm5;66779;Contributor
ariedov;958646;Contributor
brettle;118192;Contributor
cdhiraj40;75211982;Contributor
dhruvpatidar359;103873587;Contributor
edwinhere;19705425;Contributor
eirikv;4076243;Contributor
eerden;277513;Contributor
Geist5000;37940313;Contributor
IordanisKokk;72551397;Contributor
jklippel;8657220;Contributor
jannic;232606;Contributor
Foso;5015532;Contributor
JonOfUs;11487762;Contributor
CreamyCookie;3063858;Contributor
Kaligule;3586246;Contributor
kvithayathil;1056073;Contributor
luiscruz;1080714;Contributor
MStrecke;5202211;Contributor
mlasson;5814258;Contributor
schwedenmut;9077622;Contributor
M-arcel;56698158;Contributor
mgborowiec;29843126;Contributor
mo;7117;Contributor
mdeveloper20;2319126;Contributor
Mchoi8;45410115;Contributor
Gaffen;718125;Contributor
mschuetz;108637;Contributor
max-wittig;6639323;Contributor
Mengshi24;58278376;Contributor
MolarAmbiguity;10541979;Contributor
mounirlamouri;573590;Contributor
nicoolasj;63880378;Contributor
nikhil097;35090769;Contributor
nproth;48482306;Contributor
oliver;2344;Contributor
panoreak;25068506;Contributor
patrickjkennedy;8617261;Contributor
pganssle;1377457;Contributor
ortylp;470439;Contributor
RafaelBod;77226971;Contributor
ramzan;55637406;Contributor
iamrichR;44210678;Contributor
SamWhited;512573;Contributor
SebiderSushi;23618858;Contributor
selivan;1208989;Contributor
sonnayasomnambula;7716779;Contributor
sethoscope;534043;Contributor
shantanahardy;26757164;Contributor
shombando;42972338;Contributor
Silverwarriorin;46795935;Contributor
danners;116551;Contributor
corecode;177979;Contributor
vimsick;20211590;Contributor
lyallemma;25173082;Contributor
edent;837136;Contributor
atrus6;357881;Contributor
Toover;8531603;Contributor
heyyviv;56256802;Contributor
waylife;3348620;Contributor
yarons;406826;Contributor
agibault;15703733;Contributor
amhokies;3124968;Contributor
andrewc1;19559401;Contributor
axq;5077221;Contributor
chrk2205;44704035;Contributor
e-t-l;40775958;Contributor
fossterer;4236021;Contributor
sak96;26397224;Contributor
gregoryjtom;32783177;Contributor
lightonflux;1377943;Contributor
loucasal;25279797;Contributor
minusf;3632883;Contributor
NWuensche;15856197;Contributor
rubenh-be;22374542;Contributor
s3lph;5564491;Contributor
silansuslu;72400543;Contributor
struggggle;19150666;Contributor
tamizh138;26201258;Contributor
thomasdomingos;16108830;Contributor
trevortabaka;1552990;Contributor
zawad2221;32180355;Contributor
1 ByteHamster 5811634 Maintainer
2 danieloeh 968613 Original creator of AntennaPod (retired)
3 mfietz 6860662 Maintainer (retired)
4 TomHennen 5216560 Maintainer (retired)
5 orionlee 250644 Contributor
6 domingos86 9538859 Contributor
7 TacoTheDank 32376686 Contributor
8 tonytamsf 149837 Contributor
9 damoasda 46045854 Contributor
10 andersonvom 69922 Contributor
11 shortspider 5712543 Contributor
12 spacecowboy 223655 Contributor
13 ebraminio 833473 Contributor
14 asdoi 36813904 Contributor
15 patheticpat 16046 Contributor
16 brad 1614 Contributor
17 Cj-Malone 10121513 Contributor
18 maxbechtold 9162198 Contributor
19 keunes 11229646 Maintainer
20 gaul 848247 Contributor
21 qkolj 6667105 Contributor
22 pachecosf 46357909 Contributor
23 gerardolgvr 20119298 Contributor
24 johnjohndoe 144518 Contributor
25 hannesa2 3314607 Contributor
26 bws9000 262625 Contributor
27 ahangarha 11241315 Contributor
28 rharriso 570910 Contributor
29 xgouchet 818706 Contributor
30 peakvalleytech 65185819 Contributor
31 sevenmaster 12869538 Contributor
32 TheRealFalcon 153674 Contributor
33 Slinger 75751 Contributor
34 vbh 56578479 Contributor
35 jas14 569991 Contributor
36 udif 809640 Contributor
37 malockin 12814657 Contributor
38 jonasburian 15125616 Contributor
39 dirkmueller 1029152 Contributor
40 jatinkumarg 20503830 Contributor
41 peschmae0 4450993 Contributor
42 orelogo 15976578 Contributor
43 txtd 7108931 Contributor
44 ydinath 4193331 Contributor
45 CedricCabessa 365097 Contributor
46 mchelen 30691 Contributor
47 dethstar 1239177 Contributor
48 drabux 10663142 Contributor
49 saqura 1935380 Contributor
50 ueen 5067479 Contributor
51 binarytoto 75904760 Contributor
52 bibz 5141956 Contributor
53 hzulla 1705654 Contributor
54 deandreamatias 21011641 Contributor
55 MeirAtIMDDE 4421079 Contributor
56 cketti 218061 Contributor
57 egsavage 126165 Contributor
58 ligi 111600 Contributor
59 Xeitor 8825715 Contributor
60 jhenninger 197274 Contributor
61 dreiss 4121 Contributor
62 liesen 26872 Contributor
63 nereocystis 2257107 Contributor
64 rezanejati 16049370 Contributor
65 thrillfall 15801468 Contributor
66 twiceyuan 2619800 Contributor
67 JessieVela 33134794 Contributor
68 HaBaLeS 730902 Contributor
69 volhol 11587858 Contributor
70 michaelmwhite 28901334 Contributor
71 CameronBanga 611354 Contributor
72 HrBDev 25826502 Contributor
73 HolgerJeromin 2410353 Contributor
74 xisberto 1914956 Contributor
75 jmue 898577 Contributor
76 katrinleinweber 9948149 Contributor
77 LatinSuD 451487 Contributor
78 24hours 650407 Contributor
79 SosoTughushi 19908097 Contributor
80 Thom-Merrilin 76849828 Contributor
81 fabolhak 20029691 Contributor
82 archibishop 36948493 Contributor
83 alifeflow 24603829 Contributor
84 avirajrsingh 69088913 Contributor
85 toggles 14695 Contributor
86 connectety 26038710 Contributor
87 matdb 48329535 Contributor
88 damlayildiz 56313500 Contributor
89 kingargyle 177042 Contributor
90 dsmith47 14109426 Contributor
91 FarzanKh 14272565 Contributor
92 hannesaa2 18496079 Contributor
93 myslok 2098329 Contributor
94 jhunnius 9149031 Contributor
95 a1291762 327162 Contributor
96 ShadowIce 59123 Contributor
97 Niffler 8172446 Contributor
98 raghulj 57007 Contributor
99 raghulrm 5362986 Contributor
100 mamehacker 16738348 Contributor
101 skitt 2128935 Contributor
102 wseemann 2296196 Contributor
103 datavizard 44409076 Contributor
104 markamaze 17114678 Contributor
105 mohitshah3111999 42018918 Contributor
106 moralesg 14352147 Contributor
107 mr-intj 6268767 Contributor
108 tamizh143 50977879 Contributor
109 tuxayo 2678215 Contributor
110 alimemonzx 44647595 Contributor
111 dev-darrell 52300159 Contributor
112 jmdouglas 10855634 Contributor
113 olivoto 15932680 Contributor
114 PtilopsisLeucotis 54054883 Contributor
115 abhinavg1997 60095795 Contributor
116 adrns 13379985 Contributor
117 alanorth 191754 Contributor
118 alexte 7724992 Contributor
119 andrey-krutov 1488973 Contributor
120 arantius 84729 Contributor
121 BoJacobs 25435640 Contributor
122 chetan882777 36985543 Contributor
123 chrissicool 232590 Contributor
124 britiger 2057760 Contributor
125 cszucko 1810383 Contributor
126 CWftw 1498303 Contributor
127 danielm5 66779 Contributor
128 ariedov 958646 Contributor
129 brettle 118192 Contributor
130 cdhiraj40 75211982 Contributor
131 dhruvpatidar359 103873587 Contributor
132 edwinhere 19705425 Contributor
133 eirikv 4076243 Contributor
134 eerden 277513 Contributor
135 Geist5000 37940313 Contributor
136 IordanisKokk 72551397 Contributor
137 jklippel 8657220 Contributor
138 jannic 232606 Contributor
139 Foso 5015532 Contributor
140 JonOfUs 11487762 Contributor
141 CreamyCookie 3063858 Contributor
142 Kaligule 3586246 Contributor
143 kvithayathil 1056073 Contributor
144 luiscruz 1080714 Contributor
145 MStrecke 5202211 Contributor
146 mlasson 5814258 Contributor
147 schwedenmut 9077622 Contributor
148 M-arcel 56698158 Contributor
149 mgborowiec 29843126 Contributor
150 mo 7117 Contributor
151 mdeveloper20 2319126 Contributor
152 Mchoi8 45410115 Contributor
153 Gaffen 718125 Contributor
154 mschuetz 108637 Contributor
155 max-wittig 6639323 Contributor
156 Mengshi24 58278376 Contributor
157 MolarAmbiguity 10541979 Contributor
158 mounirlamouri 573590 Contributor
159 nicoolasj 63880378 Contributor
160 nikhil097 35090769 Contributor
161 nproth 48482306 Contributor
162 oliver 2344 Contributor
163 panoreak 25068506 Contributor
164 patrickjkennedy 8617261 Contributor
165 pganssle 1377457 Contributor
166 ortylp 470439 Contributor
167 RafaelBod 77226971 Contributor
168 ramzan 55637406 Contributor
169 iamrichR 44210678 Contributor
170 SamWhited 512573 Contributor
171 SebiderSushi 23618858 Contributor
172 selivan 1208989 Contributor
173 sonnayasomnambula 7716779 Contributor
174 sethoscope 534043 Contributor
175 shantanahardy 26757164 Contributor
176 shombando 42972338 Contributor
177 Silverwarriorin 46795935 Contributor
178 danners 116551 Contributor
179 corecode 177979 Contributor
180 vimsick 20211590 Contributor
181 lyallemma 25173082 Contributor
182 edent 837136 Contributor
183 atrus6 357881 Contributor
184 Toover 8531603 Contributor
185 heyyviv 56256802 Contributor
186 waylife 3348620 Contributor
187 yarons 406826 Contributor
188 agibault 15703733 Contributor
189 amhokies 3124968 Contributor
190 andrewc1 19559401 Contributor
191 axq 5077221 Contributor
192 chrk2205 44704035 Contributor
193 e-t-l 40775958 Contributor
194 fossterer 4236021 Contributor
195 sak96 26397224 Contributor
196 gregoryjtom 32783177 Contributor
197 lightonflux 1377943 Contributor
198 loucasal 25279797 Contributor
199 minusf 3632883 Contributor
200 NWuensche 15856197 Contributor
201 rubenh-be 22374542 Contributor
202 s3lph 5564491 Contributor
203 silansuslu 72400543 Contributor
204 struggggle 19150666 Contributor
205 tamizh138 26201258 Contributor
206 thomasdomingos 16108830 Contributor
207 trevortabaka 1552990 Contributor
208 zawad2221 32180355 Contributor

View File

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<libraries>
<library
name="AntennaPod"
author="The AntennaPod team"
website="https://github.com/AntennaPod/AntennaPod"
license="GPL-3.0"
licenseText="LICENSE.txt" />
<library
name="AntennaPod-AudioPlayer"
author="The AntennaPod team"
website="https://github.com/AntennaPod/AntennaPod-AudioPlayer"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Android Jetpack"
author="Google"
website="https://developer.android.com/jetpack"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Apache Commons"
author="The Apache Software Foundation"
website="https://commons.apache.org/"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Balloon"
author="Jaewoong Eum"
website="https://github.com/skydoves/Balloon"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Conscrypt"
author="Google"
website="https://github.com/google/conscrypt"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="EventBus"
author="greenrobot"
website="https://github.com/greenrobot/EventBus"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="ExoPlayer"
author="Google"
website="https://github.com/google/ExoPlayer"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Floating Action Button Speed Dial"
author="Roberto Leinardi"
website="https://github.com/leinardi/FloatingActionButtonSpeedDial"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="fyydlin"
author="Martin Fietz"
website="https://github.com/mfietz/fyydlin"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Glide"
author="bumptech"
website="https://github.com/bumptech/glide"
license="Simplified BSD"
licenseText="LICENSE_GLIDE.txt" />
<library
name="Iconify"
author="Joan Zapata"
website="https://github.com/JoanZapata/android-iconify"
license="Apache 2.0"
licenseText="LICENSE_ANDROID_ICONIFY.txt" />
<library
name="jsoup"
author="Jonathan Hedley"
website="https://jsoup.org/"
license="MIT"
licenseText="LICENSE_JSOUP.txt" />
<library
name="Lightweight-Stream-API"
author="Victor Melnik"
website="https://github.com/aNNiMON/Lightweight-Stream-API"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Material Components for Android"
author="Google"
website="https://github.com/material-components/material-components-android"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Material Design Icons"
author="Google"
website="https://github.com/google/material-design-icons"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Material Design Icons"
author="Templarian"
website="https://github.com/Templarian/MaterialDesign"
license="Pictogrammers Free License"
licenseText="LICENSE_PICTOGRAMMERS.txt" />
<library
name="OkHttp"
author="Square"
website="https://github.com/square/okhttp"
license="Apache 2.0"
licenseText="LICENSE_OKHTTP.txt" />
<library
name="Okio"
author="Square"
website="https://github.com/square/okio"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="RecyclerViewSwipeDecorator"
author="Paolo Montalto"
website="https://github.com/xabaras/RecyclerViewSwipeDecorator"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="RxAndroid"
author="ReactiveX"
website="https://github.com/ReactiveX/RxAndroid"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="RxJava"
author="ReactiveX"
website="https://github.com/ReactiveX/RxJava"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="SearchPreference"
author="ByteHamster"
website="https://github.com/ByteHamster/SearchPreference"
license="MIT"
licenseText="LICENSE_SEARCHPREFERENCE.txt" />
<library
name="StackBlur"
author="Enrique López Mañas"
website="https://github.com/kikoso/android-stackblur"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="Triangle Label View"
author="Shota Saito"
website="https://github.com/shts/TriangleLabelView"
license="Apache 2.0"
licenseText="LICENSE_TRIANGLE_LABEL_VIEW.txt" />
</libraries>

View File

@ -1,4 +0,0 @@
221 Pixels;Logo design;https://avatars2.githubusercontent.com/u/58243143?s=60&v=4
Anxhelo Lushka;Website design;https://avatars2.githubusercontent.com/u/25004151?s=60&v=4
ByteHamster;Forum admin;https://avatars2.githubusercontent.com/u/5811634?s=60&v=4
Keunes;Communications;https://avatars2.githubusercontent.com/u/11229646?s=60&v=4
1 221 Pixels Logo design https://avatars2.githubusercontent.com/u/58243143?s=60&v=4
2 Anxhelo Lushka Website design https://avatars2.githubusercontent.com/u/25004151?s=60&v=4
3 ByteHamster Forum admin https://avatars2.githubusercontent.com/u/5811634?s=60&v=4
4 Keunes Communications https://avatars2.githubusercontent.com/u/11229646?s=60&v=4

View File

@ -1,49 +0,0 @@
Arabic;abuzar3.khalid, AhmedHll, badarotti, HeshamTB, keunes, Mehyar, mh.abdelhay, mhamade, moftasa, mohmans, MustafaAlgurabi, nabilMaghura, rex07, shubbar
Asturian (ast_ES);enolp, keunes
Azerbaijani;5NOER227O, xxmn77
Basque;bipoza, gaztainalde, IngrownMink4, keunes, Osoitz, pospolos
Bengali;laggybird
Breton;Belvar, Eorn, Iriep, keunes, technozuzici
Bulgarian;keunes, ma4ko, ppk89, solusitor, x7ype
Catalan;arseru, badlop, bluegeekgh, carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70
Chinese (zh_CN);135e2, Biacke, brnme, claybiockiller, clong289734997, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, JY3, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok
Chinese (zh_TW);bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808
Czech (cs_CZ);anotheranonymoususer, befeleme, elich, Hanzmeister, jjh, McLenin666, md.share, ShimonH, svetlemodry, Thomaash, viotalJiplk
Danish;deusdenton, ERYpTION, JFreak, jhertel, keunes, mikini, petterbejo, SebastianKiwiDk
Dutch;e2jk, keunes, mijnheer, oldblue, rwv, Vistaus
Estonian;beez276, Eraser, keunes, mahfiaz
Finnish;Ban3, keunes, ktstmu, Kuutar, noppa, Sahtor, scop, teemue
French;5NOER227O, ayiniho, ChaoticMind, clombion, Cornegidouille, Daremo, e2jk, keunes, klintom, Kuscoo, lacouture, LouFex, Matth78, petterbejo, PierreLaville, Poussinou, RomainTT, sterylmreep, teamon
Galician;antiparvos, pikamoku, Raichely
German;5NOER227O, _Er, axre, ByteHamster, Ceekay, ceving, dadosch, datesastick, DerSilly, elkangaroo, enz, Erc187, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JMAN, JoeMcFly, jokap, JoniArida, JonOfUs, kalei, keunes, Kostas_F, Macusercom, max.wittig, mfietz, Michael_Strecke, mkida, petterbejo, pudeeh, Quiss42, repat, sadfgdf, Sargon_Isa, teamon, thetrash23, timo.rohwedder, toaskoas, Tobiasff3200, tomte, Tonne11, tweimer, VfBFan, Willhelm, ypid
Hebrew (he_IL);amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron
Hindi (hi_IN);keunes, purple.coder, rajs1942, siddhusengar, singhrishi245021, thelazyoxymoron
Hu;hurrikan, keunes, lna91, lomapur, marthynw, meskobalazs, naren93
Icelandic;keunes, marthjod
Indonesian;dbrw, justch, keunes, levirs565, liimee
Italian (it_IT);aalex70, allin, alvami, atilluF, Bonnee, datesastick, dontknowcris, giuseppep, Guybrush88, ilmanzo, juanjom, keunes, lu.por, m.chinni, marco_pag, mat650, mircocau, neonsoftware, niccord, salorock, theloca95
Japanese;ayiniho, keunes, KotaKato, Naofumi, sh3llc4t, tko_cactus, TranslatorG
Kannada (kn_IN);chiraag.nataraj, deepu2, keunes, thejeshgn
Ko;changwoo, eshc123, keunes, libliboom
Latin;nivaca
Lithuanian;keunes, naglis, Sharper
Macedonian;krisfremen
Malayalam;joice, keunes, KiranS, rashivkp
Modern Greek (1453-);AnimaRain, antonist, keunes, Kostas_F, pavlosv, pcguy23
Norwegian Bokmål (nb_NO);abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, Jamiera, keunes, kongk, sevenmaster, tc5, timbast, TrymSan, ttick
Persian;ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, K2latmanesh, keunes, sinamoghaddas
Polish (pl_PL);befeleme, ewm, Gadzinisko, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, millup, Rakowy_Manaska, shark103, tyle
Portuguese;emansije, jmelo461, keunes, lecalam, smarquespt, WalkerPt
Portuguese (pt_BR);alexupits, alysonborges, amalvarenga, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, jmelo461, keunes, lipefire, mbaltar, olivoto, philosp, rogervezaro, RubeensVinicius, SamWilliam, tepadilha, tschertel, ziul123
Romanian (ro_RO);AdrianMirica, fuzzmz, keunes, mozartro, ralienpp
Russian (ru_RU);ashed, btimofeev, Duke_Raven, flexagoon, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, nachoman, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea, yako
Slovak;ati3, jose1711, keunes, marulinko, McLenin666, real_name, tiborepcek
Slovenian (sl_SI);anzepintar, asovic, keunes, panter23, TheFireFighter, trus2
Spanish;5NOER227O, AleksSyntek, andersonvom, andrespelaezp, arseru, Atreyu94, badlop, CaeM0R, carlos.levy, cartojo, deandreamatias, delthia, devarops, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, kiekie, LatinSuD, leogrignafini, meanderingDot, nivaca, rafael.osuna, technozuzici, tres.14159, vfmatzkin, victorzequeida96, wakutiteo, ziul123
Swahili (macrolanguage);1silvester, keunes, kmtra
Swedish (sv_SE);aiix, bittin, bpnilsson, keunes, LinAGKar, nilso, TwoD, victorhggqvst
Tatar;seber
Telugu;keunes, veeven
Turkish;AhmedDuran, alianilkocak, AliGaygisiz, androtuna, archixe, brsata, Erdy, keunes, overbite, Slsdem
Ukrainian (uk_UA);hishak, keunes, older, paul_sm, sergiyr, voinovich_vyacheslav, zhenya97
Vietnamese;abnvolk, bruhwut, keunes, ppanhh
1 Arabic abuzar3.khalid, AhmedHll, badarotti, HeshamTB, keunes, Mehyar, mh.abdelhay, mhamade, moftasa, mohmans, MustafaAlgurabi, nabilMaghura, rex07, shubbar
2 Asturian (ast_ES) enolp, keunes
3 Azerbaijani 5NOER227O, xxmn77
4 Basque bipoza, gaztainalde, IngrownMink4, keunes, Osoitz, pospolos
5 Bengali laggybird
6 Breton Belvar, Eorn, Iriep, keunes, technozuzici
7 Bulgarian keunes, ma4ko, ppk89, solusitor, x7ype
8 Catalan arseru, badlop, bluegeekgh, carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70
9 Chinese (zh_CN) 135e2, Biacke, brnme, claybiockiller, clong289734997, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, JY3, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok
10 Chinese (zh_TW) bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808
11 Czech (cs_CZ) anotheranonymoususer, befeleme, elich, Hanzmeister, jjh, McLenin666, md.share, ShimonH, svetlemodry, Thomaash, viotalJiplk
12 Danish deusdenton, ERYpTION, JFreak, jhertel, keunes, mikini, petterbejo, SebastianKiwiDk
13 Dutch e2jk, keunes, mijnheer, oldblue, rwv, Vistaus
14 Estonian beez276, Eraser, keunes, mahfiaz
15 Finnish Ban3, keunes, ktstmu, Kuutar, noppa, Sahtor, scop, teemue
16 French 5NOER227O, ayiniho, ChaoticMind, clombion, Cornegidouille, Daremo, e2jk, keunes, klintom, Kuscoo, lacouture, LouFex, Matth78, petterbejo, PierreLaville, Poussinou, RomainTT, sterylmreep, teamon
17 Galician antiparvos, pikamoku, Raichely
18 German 5NOER227O, _Er, axre, ByteHamster, Ceekay, ceving, dadosch, datesastick, DerSilly, elkangaroo, enz, Erc187, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JMAN, JoeMcFly, jokap, JoniArida, JonOfUs, kalei, keunes, Kostas_F, Macusercom, max.wittig, mfietz, Michael_Strecke, mkida, petterbejo, pudeeh, Quiss42, repat, sadfgdf, Sargon_Isa, teamon, thetrash23, timo.rohwedder, toaskoas, Tobiasff3200, tomte, Tonne11, tweimer, VfBFan, Willhelm, ypid
19 Hebrew (he_IL) amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron
20 Hindi (hi_IN) keunes, purple.coder, rajs1942, siddhusengar, singhrishi245021, thelazyoxymoron
21 Hu hurrikan, keunes, lna91, lomapur, marthynw, meskobalazs, naren93
22 Icelandic keunes, marthjod
23 Indonesian dbrw, justch, keunes, levirs565, liimee
24 Italian (it_IT) aalex70, allin, alvami, atilluF, Bonnee, datesastick, dontknowcris, giuseppep, Guybrush88, ilmanzo, juanjom, keunes, lu.por, m.chinni, marco_pag, mat650, mircocau, neonsoftware, niccord, salorock, theloca95
25 Japanese ayiniho, keunes, KotaKato, Naofumi, sh3llc4t, tko_cactus, TranslatorG
26 Kannada (kn_IN) chiraag.nataraj, deepu2, keunes, thejeshgn
27 Ko changwoo, eshc123, keunes, libliboom
28 Latin nivaca
29 Lithuanian keunes, naglis, Sharper
30 Macedonian krisfremen
31 Malayalam joice, keunes, KiranS, rashivkp
32 Modern Greek (1453-) AnimaRain, antonist, keunes, Kostas_F, pavlosv, pcguy23
33 Norwegian Bokmål (nb_NO) abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, Jamiera, keunes, kongk, sevenmaster, tc5, timbast, TrymSan, ttick
34 Persian ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, K2latmanesh, keunes, sinamoghaddas
35 Polish (pl_PL) befeleme, ewm, Gadzinisko, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, millup, Rakowy_Manaska, shark103, tyle
36 Portuguese emansije, jmelo461, keunes, lecalam, smarquespt, WalkerPt
37 Portuguese (pt_BR) alexupits, alysonborges, amalvarenga, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, jmelo461, keunes, lipefire, mbaltar, olivoto, philosp, rogervezaro, RubeensVinicius, SamWilliam, tepadilha, tschertel, ziul123
38 Romanian (ro_RO) AdrianMirica, fuzzmz, keunes, mozartro, ralienpp
39 Russian (ru_RU) ashed, btimofeev, Duke_Raven, flexagoon, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, nachoman, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea, yako
40 Slovak ati3, jose1711, keunes, marulinko, McLenin666, real_name, tiborepcek
41 Slovenian (sl_SI) anzepintar, asovic, keunes, panter23, TheFireFighter, trus2
42 Spanish 5NOER227O, AleksSyntek, andersonvom, andrespelaezp, arseru, Atreyu94, badlop, CaeM0R, carlos.levy, cartojo, deandreamatias, delthia, devarops, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, kiekie, LatinSuD, leogrignafini, meanderingDot, nivaca, rafael.osuna, technozuzici, tres.14159, vfmatzkin, victorzequeida96, wakutiteo, ziul123
43 Swahili (macrolanguage) 1silvester, keunes, kmtra
44 Swedish (sv_SE) aiix, bittin, bpnilsson, keunes, LinAGKar, nilso, TwoD, victorhggqvst
45 Tatar seber
46 Telugu keunes, veeven
47 Turkish AhmedDuran, alianilkocak, AliGaygisiz, androtuna, archixe, brsata, Erdy, keunes, overbite, Slsdem
48 Ukrainian (uk_UA) hishak, keunes, older, paul_sm, sergiyr, voinovich_vyacheslav, zhenya97
49 Vietnamese abnvolk, bruhwut, keunes, ppanhh

View File

@ -1,5 +0,0 @@
en
fr
nl
it
da

View File

@ -5,7 +5,6 @@ import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
@ -29,7 +28,7 @@ public class ViewPagerBottomSheetBehavior<V extends View> extends BottomSheetBeh
@Override
View findScrollingChild(View view) {
if (ViewCompat.isNestedScrollingEnabled(view)) {
if (view.isNestedScrollingEnabled()) {
return view;
}

View File

@ -0,0 +1,60 @@
package de.danoeh.antennapod;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import de.danoeh.antennapod.net.download.service.episode.autodownload.AutoDownloadManagerImpl;
import de.danoeh.antennapod.net.download.service.feed.FeedUpdateManagerImpl;
import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import de.danoeh.antennapod.net.sync.service.SyncService;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink;
import de.danoeh.antennapod.storage.preferences.SynchronizationSettings;
import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials;
import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.storage.preferences.UsageStatistics;
import de.danoeh.antennapod.net.common.UserAgentInterceptor;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.net.common.AntennapodHttpClient;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.net.download.service.feed.DownloadServiceInterfaceImpl;
import de.danoeh.antennapod.net.common.NetworkUtils;
import de.danoeh.antennapod.net.ssl.SslProviderInstaller;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import de.danoeh.antennapod.ui.notifications.NotificationUtils;
import java.io.File;
public class ClientConfigurator {
private static boolean initialized = false;
public static synchronized void initialize(Context context) {
if (initialized) {
return;
}
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
UserAgentInterceptor.USER_AGENT = "AntennaPod/" + packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
PodDBAdapter.init(context);
UserPreferences.init(context);
SynchronizationCredentials.init(context);
SynchronizationSettings.init(context);
UsageStatistics.init(context);
PlaybackPreferences.init(context);
SslProviderInstaller.install(context);
NetworkUtils.init(context);
DownloadServiceInterface.setImpl(new DownloadServiceInterfaceImpl());
FeedUpdateManager.setInstance(new FeedUpdateManagerImpl());
AutoDownloadManager.setInstance(new AutoDownloadManagerImpl());
SynchronizationQueueSink.setServiceStarterImpl(() -> SyncService.sync(context));
AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
AntennapodHttpClient.setProxyConfig(UserPreferences.getProxyConfig());
SleepTimerPreferences.init(context);
NotificationUtils.createChannels(context);
initialized = true;
}
}

View File

@ -0,0 +1,67 @@
package de.danoeh.antennapod;
import android.os.Build;
import android.util.Log;
import de.danoeh.antennapod.BuildConfig;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
public class CrashReportWriter implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashReportWriter";
private final Thread.UncaughtExceptionHandler defaultHandler;
public CrashReportWriter() {
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
}
public static File getFile() {
return new File(UserPreferences.getDataFolder(null), "crash-report.log");
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
write(ex);
defaultHandler.uncaughtException(thread, ex);
}
public static void write(Throwable exception) {
File path = getFile();
PrintWriter out = null;
try {
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);
out.println();
out.println("## StackTrace");
out.println("```");
exception.printStackTrace(out);
out.println("```");
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(out);
}
}
public static String getSystemInfo() {
return "## Environment"
+ "\nAndroid version: " + Build.VERSION.RELEASE
+ "\nOS version: " + System.getProperty("os.version")
+ "\nAntennaPod version: " + BuildConfig.VERSION_NAME
+ "\nModel: " + Build.MODEL
+ "\nDevice: " + Build.DEVICE
+ "\nProduct: " + Build.PRODUCT;
}
}

View File

@ -1,44 +1,22 @@
package de.danoeh.antennapod;
import android.content.ComponentName;
import android.content.Intent;
import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import androidx.multidex.MultiDexApplication;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.fonts.FontAwesomeModule;
import com.joanzapata.iconify.fonts.MaterialModule;
import com.google.android.material.color.DynamicColors;
import de.danoeh.antennapod.activity.SplashActivity;
import de.danoeh.antennapod.core.ApCoreEventBusIndex;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.error.CrashReportWriter;
import de.danoeh.antennapod.error.RxJavaErrorHandlerSetup;
import de.danoeh.antennapod.spa.SPAUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.EventBusException;
/** Main application class. */
public class PodcastApp extends MultiDexApplication {
// make sure that ClientConfigurator executes its static code
static {
try {
Class.forName("de.danoeh.antennapod.config.ClientConfigurator");
} catch (Exception e) {
throw new RuntimeException("ClientConfigurator not found", e);
}
}
private static PodcastApp singleton;
public static PodcastApp getInstance() {
return singleton;
}
public class PodcastApp extends Application {
private static final String TAG = "PodcastApp";
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(new CrashReportWriter());
RxJavaErrorHandlerSetup.setupRxJavaErrorHandler();
@ -53,28 +31,20 @@ public class PodcastApp extends MultiDexApplication {
StrictMode.setVmPolicy(builder.build());
}
singleton = this;
ClientConfig.initialize(this);
Iconify.with(new FontAwesomeModule());
Iconify.with(new MaterialModule());
try {
// Robolectric calls onCreate for every test, which causes problems with static members
EventBus.builder()
.addIndex(new ApEventBusIndex())
.logNoSubscriberMessages(false)
.sendNoSubscriberEvent(false)
.installDefaultEventBus();
} catch (EventBusException e) {
Log.d(TAG, e.getMessage());
}
DynamicColors.applyToActivitiesIfAvailable(this);
ClientConfigurator.initialize(this);
PreferenceUpgrader.checkUpgrades(this);
SPAUtil.sendSPAppsQueryFeedsIntent(this);
EventBus.builder()
.addIndex(new ApEventBusIndex())
.addIndex(new ApCoreEventBusIndex())
.logNoSubscriberMessages(false)
.sendNoSubscriberEvent(false)
.installDefaultEventBus();
}
public static void forceRestart() {
Intent intent = new Intent(getInstance(), SplashActivity.class);
ComponentName cn = intent.getComponent();
Intent mainIntent = Intent.makeRestartActivityTask(cn);
getInstance().startActivity(mainIntent);
Runtime.getRuntime().exit(0);
}
}

View File

@ -0,0 +1,170 @@
package de.danoeh.antennapod;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.KeyEvent;
import androidx.core.app.NotificationManagerCompat;
import androidx.preference.PreferenceManager;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.CrashReportWriter;
import de.danoeh.antennapod.ui.screen.AllEpisodesFragment;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation;
import de.danoeh.antennapod.ui.screen.queue.QueueFragment;
import de.danoeh.antennapod.ui.swipeactions.SwipeAction;
import de.danoeh.antennapod.ui.swipeactions.SwipeActions;
public class PreferenceUpgrader {
private static final String PREF_CONFIGURED_VERSION = "version_code";
private static final String PREF_NAME = "app_version";
private static SharedPreferences prefs;
public static void checkUpgrades(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences upgraderPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, -1);
int newVersion = BuildConfig.VERSION_CODE;
if (oldVersion != newVersion) {
CrashReportWriter.getFile().delete();
upgrade(oldVersion, context);
upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply();
}
}
private static void upgrade(int oldVersion, Context context) {
if (oldVersion == -1) {
//New installation
return;
}
if (oldVersion < 1070196) {
// migrate episode cleanup value (unit changed from days to hours)
int oldValueInDays = UserPreferences.getEpisodeCleanupValue();
if (oldValueInDays > 0) {
UserPreferences.setEpisodeCleanupValue(oldValueInDays * 24);
} // else 0 or special negative values, no change needed
}
if (oldVersion < 1070197) {
if (prefs.getBoolean("prefMobileUpdate", false)) {
prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply();
}
}
if (oldVersion < 1070300) {
if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) {
UserPreferences.setAllowMobileAutoDownload(true);
}
switch (prefs.getString("prefMobileUpdateAllowed", "images")) {
case "everything":
UserPreferences.setAllowMobileFeedRefresh(true);
UserPreferences.setAllowMobileEpisodeDownload(true);
UserPreferences.setAllowMobileImages(true);
break;
case "images":
UserPreferences.setAllowMobileImages(true);
break;
case "nothing":
UserPreferences.setAllowMobileImages(false);
break;
}
}
if (oldVersion < 1070400) {
UserPreferences.ThemePreference theme = UserPreferences.getTheme();
if (theme == UserPreferences.ThemePreference.LIGHT) {
prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply();
}
UserPreferences.setQueueLocked(false);
UserPreferences.setStreamOverDownload(false);
if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) {
final String keyOldPrefEnqueueFront = "prefQueueAddToFront";
boolean enqueueAtFront = prefs.getBoolean(keyOldPrefEnqueueFront, false);
EnqueueLocation enqueueLocation = enqueueAtFront ? EnqueueLocation.FRONT : EnqueueLocation.BACK;
UserPreferences.setEnqueueLocation(enqueueLocation);
}
}
if (oldVersion < 2010300) {
// Migrate hardware button preferences
if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) {
prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON,
String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply();
}
if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) {
prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON,
String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply();
}
}
if (oldVersion < 2040000) {
SharedPreferences swipePrefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
swipePrefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG,
SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply();
}
if (oldVersion < 2050000) {
prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply();
}
if (oldVersion < 2080000) {
// Migrate drawer feed counter setting to reflect removal of
// "unplayed and in inbox" (0), by changing it to "unplayed" (2)
String feedCounterSetting = prefs.getString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "1");
if (feedCounterSetting.equals("0")) {
prefs.edit().putString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2").apply();
}
SharedPreferences sleepTimerPreferences =
context.getSharedPreferences(SleepTimerPreferences.PREF_NAME, Context.MODE_PRIVATE);
TimeUnit[] timeUnits = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS };
long value = Long.parseLong(SleepTimerPreferences.lastTimerValue());
TimeUnit unit = timeUnits[sleepTimerPreferences.getInt("LastTimeUnit", 1)];
SleepTimerPreferences.setLastTimer(String.valueOf(unit.toMinutes(value)));
if (prefs.getString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "20")
.equals(context.getString(R.string.pref_episode_cache_unlimited))) {
prefs.edit().putString(UserPreferences.PREF_EPISODE_CACHE_SIZE,
"" + UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED).apply();
}
}
if (oldVersion < 3000007) {
if (prefs.getString("prefBackButtonBehavior", "").equals("drawer")) {
prefs.edit().putBoolean(UserPreferences.PREF_BACK_OPENS_DRAWER, true).apply();
}
}
if (oldVersion < 3010000) {
if (prefs.getString(UserPreferences.PREF_THEME, "system").equals("2")) {
prefs.edit()
.putString(UserPreferences.PREF_THEME, "1")
.putBoolean(UserPreferences.PREF_THEME_BLACK, true)
.apply();
}
UserPreferences.setAllowMobileSync(true);
if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":").contains(":")) { // Unset or "time of day"
prefs.edit().putString(UserPreferences.PREF_UPDATE_INTERVAL, "12").apply();
}
}
if (oldVersion < 3020000) {
NotificationManagerCompat.from(context).deleteNotificationChannel("auto_download");
}
if (oldVersion < 3030000) {
SharedPreferences allEpisodesPreferences =
context.getSharedPreferences(AllEpisodesFragment.PREF_NAME, Context.MODE_PRIVATE);
String oldEpisodeSort = allEpisodesPreferences.getString(UserPreferences.PREF_SORT_ALL_EPISODES, "");
if (!StringUtils.isAllEmpty(oldEpisodeSort)) {
prefs.edit().putString(UserPreferences.PREF_SORT_ALL_EPISODES, oldEpisodeSort).apply();
}
String oldEpisodeFilter = allEpisodesPreferences.getString("filter", "");
if (!StringUtils.isAllEmpty(oldEpisodeFilter)) {
prefs.edit().putString(UserPreferences.PREF_FILTER_ALL_EPISODES, oldEpisodeFilter).apply();
}
}
}
}

View File

@ -0,0 +1,35 @@
package de.danoeh.antennapod;
import android.util.Log;
import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.plugins.RxJavaPlugins;
public class RxJavaErrorHandlerSetup {
private static final String TAG = "RxJavaErrorHandler";
private RxJavaErrorHandlerSetup() {
}
public static void setupRxJavaErrorHandler() {
RxJavaPlugins.setErrorHandler(exception -> {
if (exception instanceof UndeliverableException) {
// Probably just disposed because the fragment was left
Log.d(TAG, "Ignored exception: " + Log.getStackTraceString(exception));
return;
}
// Usually, undeliverable exceptions are wrapped in an UndeliverableException.
// If an undeliverable exception is a NPE (or some others), wrapping does not happen.
// AntennaPod threads might throw NPEs after disposing because we set controllers to null.
// Just swallow all exceptions here.
Log.e(TAG, Log.getStackTraceString(exception));
CrashReportWriter.write(exception);
if (BuildConfig.DEBUG) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), exception);
}
});
}
}

View File

@ -0,0 +1,41 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.storage.database.DBWriter;
public class CancelDownloadActionButton extends ItemActionButton {
public CancelDownloadActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.cancel_download_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_cancel;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
DownloadServiceInterface.get().cancel(context, media);
if (UserPreferences.isEnableAutodownload()) {
item.disableAutoDownload();
DBWriter.setFeedItem(item);
}
}
}

View File

@ -0,0 +1,53 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import java.util.Collections;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.ui.view.LocalDeleteModal;
public class DeleteActionButton extends ItemActionButton {
public DeleteActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.delete_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_delete;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(context, Collections.singletonList(item),
() -> DBWriter.deleteFeedMediaOfItem(context, media));
}
@Override
public int getVisibility() {
if (item.getMedia() != null && (item.getMedia().isDownloaded() || item.getFeed().isLocalFeed())) {
return View.VISIBLE;
}
return View.INVISIBLE;
}
}

View File

@ -0,0 +1,74 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.storage.preferences.UsageStatistics;
import de.danoeh.antennapod.net.common.NetworkUtils;
public class DownloadActionButton extends ItemActionButton {
public DownloadActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.download_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_download;
}
@Override
public int getVisibility() {
return item.getFeed().isLocalFeed() ? View.INVISIBLE : View.VISIBLE;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null || shouldNotDownload(media)) {
return;
}
UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD);
if (NetworkUtils.isEpisodeDownloadAllowed()) {
DownloadServiceInterface.get().downloadNow(context, item, false);
} else {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.confirm_mobile_download_dialog_title)
.setPositiveButton(R.string.confirm_mobile_download_dialog_download_later,
(d, w) -> DownloadServiceInterface.get().downloadNow(context, item, false))
.setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time,
(d, w) -> DownloadServiceInterface.get().downloadNow(context, item, true))
.setNegativeButton(R.string.cancel_label, null);
if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) {
builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn);
} else {
builder.setMessage(R.string.confirm_mobile_download_dialog_message);
}
builder.show();
}
}
private boolean shouldNotDownload(@NonNull FeedMedia media) {
boolean isDownloading = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl());
return isDownloading || media.isDownloaded();
}
}

View File

@ -0,0 +1,67 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import android.view.View;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.playback.service.PlaybackStatus;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
public abstract class ItemActionButton {
FeedItem item;
ItemActionButton(FeedItem item) {
this.item = item;
}
@StringRes
public abstract int getLabel();
@DrawableRes
public abstract int getDrawable();
public abstract void onClick(Context context);
public int getVisibility() {
return View.VISIBLE;
}
@NonNull
public static ItemActionButton forItem(@NonNull FeedItem item) {
final FeedMedia media = item.getMedia();
if (media == null) {
return new MarkAsPlayedActionButton(item);
}
final boolean isDownloadingMedia = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownloadUrl());
if (PlaybackStatus.isCurrentlyPlaying(media)) {
return new PauseActionButton(item);
} else if (item.getFeed().isLocalFeed()) {
return new PlayLocalActionButton(item);
} else if (media.isDownloaded()) {
return new PlayActionButton(item);
} else if (isDownloadingMedia) {
return new CancelDownloadActionButton(item);
} else if (item.getFeed().getState() != Feed.STATE_SUBSCRIBED) {
return new StreamActionButton(item);
} else if (UserPreferences.isStreamOverDownload()) {
return new StreamActionButton(item);
} else {
return new DownloadActionButton(item);
}
}
public void configure(@NonNull View button, @NonNull ImageView icon, Context context) {
button.setVisibility(getVisibility());
button.setContentDescription(context.getString(getLabel()));
button.setOnClickListener((view) -> onClick(context));
icon.setImageResource(getDrawable());
}
}

View File

@ -0,0 +1,41 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import android.view.View;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.storage.database.DBWriter;
public class MarkAsPlayedActionButton extends ItemActionButton {
public MarkAsPlayedActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return (item.hasMedia() ? R.string.mark_read_label : R.string.mark_read_no_media_label);
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_check;
}
@Override
public void onClick(Context context) {
if (!item.isPlayed()) {
DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
}
}
@Override
public int getVisibility() {
return (item.isPlayed()) ? View.INVISIBLE : View.VISIBLE;
}
}

View File

@ -0,0 +1,42 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.view.KeyEvent;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.playback.service.PlaybackStatus;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter;
public class PauseActionButton extends ItemActionButton {
public PauseActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.pause_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_pause;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
if (media == null) {
return;
}
if (PlaybackStatus.isCurrentlyPlaying(media)) {
context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE));
}
}
}

View File

@ -0,0 +1,60 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.playback.service.PlaybackService;
import de.danoeh.antennapod.playback.service.PlaybackServiceStarter;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
import org.greenrobot.eventbus.EventBus;
public class PlayActionButton extends ItemActionButton {
private static final String TAG = "PlayActionButton";
public PlayActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.play_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_play_24dp;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
if (media == null) {
return;
}
if (!media.fileExists()) {
Log.i(TAG, "Missing episode. Will update the database now.");
media.setDownloaded(false, 0);
media.setLocalFileUrl(null);
DBWriter.setFeedMedia(media);
EventBus.getDefault().post(FeedItemEvent.updated(media.getItem()));
EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found)));
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -0,0 +1,46 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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.playback.service.PlaybackService;
import de.danoeh.antennapod.playback.service.PlaybackServiceStarter;
public class PlayLocalActionButton extends ItemActionButton {
public PlayLocalActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.play_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_play_24dp;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -0,0 +1,56 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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.playback.service.PlaybackService;
import de.danoeh.antennapod.playback.service.PlaybackServiceStarter;
import de.danoeh.antennapod.storage.preferences.UsageStatistics;
import de.danoeh.antennapod.net.common.NetworkUtils;
import de.danoeh.antennapod.ui.StreamingConfirmationDialog;
public class StreamActionButton extends ItemActionButton {
public StreamActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.stream_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_stream;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
UsageStatistics.logAction(UsageStatistics.ACTION_STREAM);
if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(context, media).show();
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -0,0 +1,38 @@
package de.danoeh.antennapod.actionbutton;
import android.content.Context;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.ui.common.IntentUtils;
public class VisitWebsiteActionButton extends ItemActionButton {
public VisitWebsiteActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.visit_website_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_web;
}
@Override
public void onClick(Context context) {
IntentUtils.openInBrowser(context, item.getLink());
}
@Override
public int getVisibility() {
return (item.getLink() == null) ? View.INVISIBLE : View.VISIBLE;
}
}

View File

@ -1,123 +0,0 @@
package de.danoeh.antennapod.activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.google.android.material.snackbar.Snackbar;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import de.danoeh.antennapod.error.CrashReportWriter;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.IntentUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* Displays the 'crash report' screen
*/
public class BugReportActivity extends AppCompatActivity {
private static final String TAG = "BugReportActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayShowHomeEnabled(true);
setContentView(R.layout.bug_report);
String stacktrace = "No crash report recorded";
try {
File crashFile = CrashReportWriter.getFile();
if (crashFile.exists()) {
stacktrace = IOUtils.toString(new FileInputStream(crashFile), Charset.forName("UTF-8"));
} else {
Log.d(TAG, stacktrace);
}
} catch (IOException e) {
e.printStackTrace();
}
TextView crashDetailsTextView = findViewById(R.id.crash_report_logs);
crashDetailsTextView.setText(CrashReportWriter.getSystemInfo() + "\n\n" + stacktrace);
findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> IntentUtils.openInBrowser(
BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues"));
findViewById(R.id.btn_copy_log).setOnClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.getText());
clipboard.setPrimaryClip(clip);
Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show();
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.bug_report_options, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.export_logcat) {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this);
alertBuilder.setMessage(R.string.confirm_export_log_dialog_message);
alertBuilder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
exportLog();
dialog.dismiss();
});
alertBuilder.setNegativeButton(R.string.cancel_label, null);
alertBuilder.show();
return true;
}
return super.onOptionsItemSelected(item);
}
private void exportLog() {
try {
File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt");
String cmd = "logcat -d -f " + filename.getAbsolutePath();
Runtime.getRuntime().exec(cmd);
//share file
try {
String authority = getString(R.string.provider_authority);
Uri fileUri = FileProvider.getUriForFile(this, authority, filename);
new ShareCompat.IntentBuilder(this)
.setType("text/*")
.addStream(fileUri)
.setChooserTitle(R.string.share_file_label)
.startChooser();
} catch (Exception e) {
e.printStackTrace();
int strResId = R.string.log_file_share_exception;
Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG)
.show();
}
} catch (IOException e) {
e.printStackTrace();
Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show();
}
}
}

View File

@ -1,76 +0,0 @@
package de.danoeh.antennapod.activity;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.appcompat.app.AppCompatActivity;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.download.DownloadService;
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;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.Validate;
/**
* Shows a username and a password text field.
* The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value.
*/
public class DownloadAuthenticationActivity extends AppCompatActivity {
/**
* The download request object that contains information about the resource that requires a username and a password.
*/
public static final String ARG_DOWNLOAD_REQUEST = "request";
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTranslucentTheme());
super.onCreate(savedInstanceState);
Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing");
DownloadRequest request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST);
new AuthenticationDialog(this, R.string.authentication_label, true, "", "") {
@Override
protected void onConfirmed(String username, String password) {
Completable.fromAction(
() -> {
request.setUsername(username);
request.setPassword(password);
if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
long mediaId = request.getFeedfileId();
FeedMedia media = DBReader.getFeedMedia(mediaId);
if (media != null) {
FeedPreferences preferences = media.getItem().getFeed().getPreferences();
if (TextUtils.isEmpty(preferences.getPassword())
|| TextUtils.isEmpty(preferences.getUsername())) {
preferences.setUsername(username);
preferences.setPassword(password);
DBWriter.setFeedPreferences(preferences);
}
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
DownloadService.download(DownloadAuthenticationActivity.this, false, request);
finish();
});
}
@Override
protected void onCancelled() {
finish();
}
}.show();
}
}

View File

@ -1,6 +1,5 @@
package de.danoeh.antennapod.activity;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -9,8 +8,6 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
@ -18,55 +15,66 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.RecyclerView;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.bumptech.glide.Glide;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.net.download.service.feed.FeedUpdateManagerImpl;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink;
import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter;
import de.danoeh.antennapod.ui.common.ThemeSwitcher;
import de.danoeh.antennapod.ui.screen.rating.RatingDialogManager;
import de.danoeh.antennapod.event.EpisodeDownloadEvent;
import de.danoeh.antennapod.event.FeedUpdateRunningEvent;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.ui.screen.AddFeedFragment;
import de.danoeh.antennapod.ui.screen.AllEpisodesFragment;
import de.danoeh.antennapod.ui.screen.playback.audio.AudioPlayerFragment;
import de.danoeh.antennapod.ui.screen.download.CompletedDownloadsFragment;
import de.danoeh.antennapod.ui.screen.download.DownloadLogFragment;
import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment;
import de.danoeh.antennapod.ui.screen.InboxFragment;
import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment;
import de.danoeh.antennapod.ui.screen.PlaybackHistoryFragment;
import de.danoeh.antennapod.ui.screen.queue.QueueFragment;
import de.danoeh.antennapod.ui.screen.SearchFragment;
import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionFragment;
import de.danoeh.antennapod.ui.TransitionEffect;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.storage.importexport.AutomaticDatabaseExportWorker;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.ui.discovery.DiscoveryFragment;
import de.danoeh.antennapod.ui.screen.home.HomeFragment;
import de.danoeh.antennapod.ui.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;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.AudioPlayerFragment;
import de.danoeh.antennapod.fragment.InboxFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SearchFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.fragment.TransitionEffect;
import de.danoeh.antennapod.preferences.PreferenceUpgrader;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.LockableBottomSheetBehavior;
import java.util.HashMap;
import java.util.Map;
/**
* The activity that is shown when the user launches the app.
@ -79,11 +87,8 @@ public class MainActivity extends CastEnabledActivity {
public static final String PREF_NAME = "MainActivityPrefs";
public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch";
public static final String EXTRA_FRAGMENT_TAG = "fragment_tag";
public static final String EXTRA_FRAGMENT_ARGS = "fragment_args";
public static final String EXTRA_FEED_ID = "fragment_feed_id";
public static final String EXTRA_REFRESH_ON_START = "refresh_on_start";
public static final String EXTRA_STARTED_FROM_SEARCH = "started_from_search";
public static final String EXTRA_ADD_TO_BACK_STACK = "add_to_back_stack";
public static final String KEY_GENERATED_VIEW_ID = "generated_view_id";
@ -91,9 +96,9 @@ public class MainActivity extends CastEnabledActivity {
private @Nullable ActionBarDrawerToggle drawerToggle;
private View navDrawer;
private LockableBottomSheetBehavior sheetBehavior;
private long lastBackButtonPressTime = 0;
private RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
private int lastTheme = 0;
private Insets navigationBarInsets = Insets.NONE;
@NonNull
public static Intent getIntentToOpenFeed(@NonNull Context context, long feedId) {
@ -105,11 +110,12 @@ public class MainActivity extends CastEnabledActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
lastTheme = UserPreferences.getNoTitleTheme();
lastTheme = ThemeSwitcher.getNoTitleTheme(this);
setTheme(lastTheme);
if (savedInstanceState != null) {
ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0));
}
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25);
@ -118,19 +124,32 @@ public class MainActivity extends CastEnabledActivity {
navDrawer = findViewById(R.id.navDrawerFragment);
setNavDrawerSize();
// Consume navigation bar insets - we apply them in setPlayerVisible()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_view), (v, insets) -> {
navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
updateInsets();
return new WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE)
.build();
});
final FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(MAIN_FRAGMENT_TAG) == null) {
String lastFragment = NavDrawerFragment.getLastNavFragment(this);
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) {
loadFragment(lastFragment, null);
if (!UserPreferences.DEFAULT_PAGE_REMEMBER.equals(UserPreferences.getDefaultPage())) {
loadFragment(UserPreferences.getDefaultPage(), null);
} else {
try {
loadFeedFragmentById(Integer.parseInt(lastFragment), null);
} catch (NumberFormatException e) {
// it's not a number, this happens if we removed
// a label from the NAV_DRAWER_TAGS
// give them a nice default...
loadFragment(QueueFragment.TAG, null);
String lastFragment = NavDrawerFragment.getLastNavFragment(this);
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) {
loadFragment(lastFragment, null);
} else {
try {
loadFeedFragmentById(Integer.parseInt(lastFragment), null);
} catch (NumberFormatException e) {
// it's not a number, this happens if we removed
// a label from the NAV_DRAWER_TAGS
// give them a nice default...
loadFragment(HomeFragment.TAG, null);
}
}
}
}
@ -143,12 +162,67 @@ public class MainActivity extends CastEnabledActivity {
transaction.commit();
checkFirstLaunch();
PreferenceUpgrader.checkUpgrades(this);
View bottomSheet = findViewById(R.id.audioplayerFragment);
sheetBehavior = (LockableBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet);
sheetBehavior.setPeekHeight((int) getResources().getDimension(R.dimen.external_player_height));
sheetBehavior.setHideable(false);
sheetBehavior.setBottomSheetCallback(bottomSheetCallback);
FeedUpdateManager.getInstance().restartUpdateAlarm(this, false);
SynchronizationQueueSink.syncNowIfNotSyncedRecently();
AutomaticDatabaseExportWorker.enqueueIfNeeded(this, false);
WorkManager.getInstance(this)
.getWorkInfosByTagLiveData(FeedUpdateManagerImpl.WORK_TAG_FEED_UPDATE)
.observe(this, workInfos -> {
boolean isRefreshingFeeds = false;
for (WorkInfo workInfo : workInfos) {
if (workInfo.getState() == WorkInfo.State.RUNNING) {
isRefreshingFeeds = true;
} else if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
isRefreshingFeeds = true;
}
}
EventBus.getDefault().postSticky(new FeedUpdateRunningEvent(isRefreshingFeeds));
});
WorkManager.getInstance(this)
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
.observe(this, workInfos -> {
Map<String, DownloadStatus> updatedEpisodes = new HashMap<>();
for (WorkInfo workInfo : workInfos) {
String downloadUrl = null;
for (String tag : workInfo.getTags()) {
if (tag.startsWith(DownloadServiceInterface.WORK_TAG_EPISODE_URL)) {
downloadUrl = tag.substring(DownloadServiceInterface.WORK_TAG_EPISODE_URL.length());
}
}
if (downloadUrl == null) {
continue;
}
int status;
if (workInfo.getState() == WorkInfo.State.RUNNING) {
status = DownloadStatus.STATE_RUNNING;
} else if (workInfo.getState() == WorkInfo.State.ENQUEUED
|| workInfo.getState() == WorkInfo.State.BLOCKED) {
status = DownloadStatus.STATE_QUEUED;
} else {
status = DownloadStatus.STATE_COMPLETED;
}
int progress = workInfo.getProgress().getInt(DownloadServiceInterface.WORK_DATA_PROGRESS, -1);
if (progress == -1 && status != DownloadStatus.STATE_COMPLETED) {
status = DownloadStatus.STATE_QUEUED;
progress = 0;
}
updatedEpisodes.put(downloadUrl, new DownloadStatus(status, progress));
}
DownloadServiceInterface.get().setCurrentDownloads(updatedEpisodes);
EventBus.getDefault().postSticky(new EpisodeDownloadEvent(updatedEpisodes));
});
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
updateInsets();
}
/**
@ -170,8 +244,9 @@ public class MainActivity extends CastEnabledActivity {
outState.putInt(KEY_GENERATED_VIEW_ID, View.generateViewId());
}
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
new BottomSheetBehavior.BottomSheetCallback() {
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback = new AntennaPodBottomSheetCallback();
private class AntennaPodBottomSheetCallback extends BottomSheetBehavior.BottomSheetCallback {
@Override
public void onStateChanged(@NonNull View view, int state) {
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
@ -193,14 +268,11 @@ public class MainActivity extends CastEnabledActivity {
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(
condensedSlideOffset > 0.99f ? View.GONE : View.VISIBLE);
audioPlayer.fadePlayerToToolbar(slideOffset);
}
};
}
public void setupToolbarToggle(@NonNull Toolbar toolbar, boolean displayUpArrow) {
public void setupToolbarToggle(@NonNull MaterialToolbar toolbar, boolean displayUpArrow) {
if (drawerLayout != null) { // Tablet layout does not have a drawer
if (drawerToggle != null) {
drawerLayout.removeDrawerListener(drawerToggle);
@ -219,19 +291,18 @@ public class MainActivity extends CastEnabledActivity {
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (drawerLayout != null && drawerToggle != null) {
drawerLayout.removeDrawerListener(drawerToggle);
}
}
private void checkFirstLaunch() {
SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) {
loadFragment(AddFeedFragment.TAG, null);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (drawerLayout != null) { // Tablet layout does not have a drawer
drawerLayout.openDrawer(navDrawer);
}
}, 1500);
// for backward compatibility, we only change defaults for fresh installs
UserPreferences.setUpdateInterval(12);
AutoUpdateManager.restartUpdateAlarm(this);
FeedUpdateManager.getInstance().restartUpdateAlarm(this, true);
SharedPreferences.Editor edit = prefs.edit();
edit.putBoolean(PREF_IS_FIRST_LAUNCH, false);
@ -247,12 +318,29 @@ public class MainActivity extends CastEnabledActivity {
return sheetBehavior;
}
private void updateInsets() {
setPlayerVisible(findViewById(R.id.audioplayerFragment).getVisibility() == View.VISIBLE);
int playerHeight = (int) getResources().getDimension(R.dimen.external_player_height);
sheetBehavior.setPeekHeight(playerHeight + navigationBarInsets.bottom);
}
public void setPlayerVisible(boolean visible) {
getBottomSheet().setLocked(!visible);
if (visible) {
bottomSheetCallback.onStateChanged(null, getBottomSheet().getState()); // Update toolbar visibility
} else {
getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
}
FragmentContainerView mainView = findViewById(R.id.main_view);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mainView.getLayoutParams();
params.setMargins(0, 0, 0, visible ? (int) getResources().getDimension(R.dimen.external_player_height) : 0);
int externalPlayerHeight = (int) getResources().getDimension(R.dimen.external_player_height);
params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right,
navigationBarInsets.bottom + (visible ? externalPlayerHeight : 0));
mainView.setLayoutParams(params);
FragmentContainerView playerView = findViewById(R.id.playerFragment);
ViewGroup.MarginLayoutParams playerParams = (ViewGroup.MarginLayoutParams) playerView.getLayoutParams();
playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0);
playerView.setLayoutParams(playerParams);
findViewById(R.id.audioplayerFragment).setVisibility(visible ? View.VISIBLE : View.GONE);
}
@ -260,10 +348,13 @@ public class MainActivity extends CastEnabledActivity {
return recycledViewPool;
}
public void loadFragment(String tag, Bundle args) {
public Fragment createFragmentInstance(String tag, Bundle args) {
Log.d(TAG, "loadFragment(tag: " + tag + ", args: " + args + ")");
Fragment fragment;
switch (tag) {
case HomeFragment.TAG:
fragment = new HomeFragment();
break;
case QueueFragment.TAG:
fragment = new QueueFragment();
break;
@ -285,19 +376,24 @@ public class MainActivity extends CastEnabledActivity {
case SubscriptionFragment.TAG:
fragment = new SubscriptionFragment();
break;
case DiscoveryFragment.TAG:
fragment = new DiscoveryFragment();
break;
default:
// default to the queue
fragment = new QueueFragment();
tag = QueueFragment.TAG;
// default to home screen
fragment = new HomeFragment();
args = null;
break;
}
if (args != null) {
fragment.setArguments(args);
}
return fragment;
}
public void loadFragment(String tag, Bundle args) {
NavDrawerFragment.saveLastNavFragment(this, tag);
loadFragment(fragment);
loadFragment(createFragmentInstance(tag, args));
}
public void loadFeedFragmentById(long feedId, Bundle args) {
@ -309,7 +405,7 @@ public class MainActivity extends CastEnabledActivity {
loadFragment(fragment);
}
private void loadFragment(Fragment fragment) {
public void loadFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
// clear back stack
for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
@ -406,25 +502,27 @@ public class MainActivity extends CastEnabledActivity {
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
RatingDialog.init(this);
new RatingDialogManager(this).showIfNeeded();
}
@Override
protected void onResume() {
super.onResume();
handleNavIntent();
RatingDialog.check();
if (lastTheme != UserPreferences.getNoTitleTheme()) {
if (lastTheme != ThemeSwitcher.getNoTitleTheme(this)) {
finish();
startActivity(new Intent(this, MainActivity.class));
}
if (UserPreferences.getHiddenDrawerItems().contains(NavDrawerFragment.getLastNavFragment(this))) {
loadFragment(UserPreferences.getDefaultPage(), null);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
lastTheme = UserPreferences.getNoTitleTheme(); // Don't recreate activity when a result is pending
lastTheme = ThemeSwitcher.getNoTitleTheme(this); // Don't recreate activity when a result is pending
}
@Override
@ -433,7 +531,6 @@ public class MainActivity extends CastEnabledActivity {
EventBus.getDefault().unregister(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
@ -462,43 +559,23 @@ public class MainActivity extends CastEnabledActivity {
@Override
public void onBackPressed() {
if (isDrawerOpen()) {
if (isDrawerOpen() && drawerLayout != null) {
drawerLayout.closeDrawer(navDrawer);
} else if (sheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (getSupportFragmentManager().getBackStackEntryCount() != 0) {
super.onBackPressed();
} else {
switch (UserPreferences.getBackButtonBehavior()) {
case OPEN_DRAWER:
if (drawerLayout != null) { // Tablet layout does not have drawer
drawerLayout.openDrawer(navDrawer);
}
break;
case SHOW_PROMPT:
new AlertDialog.Builder(this)
.setMessage(R.string.close_prompt)
.setPositiveButton(R.string.yes, (dialogInterface, i) -> MainActivity.super.onBackPressed())
.setNegativeButton(R.string.no, null)
.setCancelable(false)
.show();
break;
case DOUBLE_TAP:
if (lastBackButtonPressTime < System.currentTimeMillis() - 2000) {
Toast.makeText(this, R.string.double_tap_toast, Toast.LENGTH_SHORT).show();
lastBackButtonPressTime = System.currentTimeMillis();
} else {
super.onBackPressed();
}
break;
case GO_TO_PAGE:
if (NavDrawerFragment.getLastNavFragment(this).equals(UserPreferences.getBackButtonGoToPage())) {
super.onBackPressed();
} else {
loadFragment(UserPreferences.getBackButtonGoToPage(), null);
}
break;
default: super.onBackPressed();
String toPage = UserPreferences.getDefaultPage();
if (NavDrawerFragment.getLastNavFragment(this).equals(toPage)
|| UserPreferences.DEFAULT_PAGE_REMEMBER.equals(toPage)) {
if (UserPreferences.backButtonOpensDrawer() && drawerLayout != null) {
drawerLayout.openDrawer(navDrawer);
} else {
super.onBackPressed();
}
} else {
loadFragment(toPage, null);
}
}
}
@ -507,42 +584,55 @@ public class MainActivity extends CastEnabledActivity {
public void onEventMainThread(MessageEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
Snackbar snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_SHORT);
Snackbar snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG);
if (event.action != null) {
snackbar.setAction(getString(R.string.undo), v -> event.action.run());
snackbar.setAction(event.actionText, v -> event.action.accept(this));
}
}
private void handleNavIntent() {
Log.d(TAG, "handleNavIntent()");
Intent intent = getIntent();
if (intent.hasExtra(EXTRA_FEED_ID) || intent.hasExtra(EXTRA_FRAGMENT_TAG) || intent.hasExtra(EXTRA_REFRESH_ON_START)) {
Log.d(TAG, "handleNavIntent()");
String tag = intent.getStringExtra(EXTRA_FRAGMENT_TAG);
Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS);
boolean refreshOnStart = intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false);
if (refreshOnStart) {
AutoUpdateManager.runImmediate(this);
}
if (intent.hasExtra(EXTRA_FEED_ID)) {
long feedId = intent.getLongExtra(EXTRA_FEED_ID, 0);
if (tag != null) {
loadFragment(tag, args);
} else if (feedId > 0) {
boolean startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false);
Bundle args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS);
if (feedId > 0) {
boolean addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false);
if (startedFromSearch || addToBackStack) {
if (addToBackStack) {
loadChildFragment(FeedItemlistFragment.newInstance(feedId));
} else {
loadFeedFragmentById(feedId, args);
}
}
sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)) {
String tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG);
Bundle args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS);
if (tag != null) {
Fragment fragment = createFragmentInstance(tag, args);
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_ADD_TO_BACK_STACK, false)) {
loadChildFragment(fragment);
} else {
loadFragment(fragment);
}
}
sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) {
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
bottomSheetCallback.onSlide(null, 1.0f);
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
} else {
handleDeeplink(intent.getData());
}
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false) && drawerLayout != null) {
drawerLayout.open();
}
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) {
new DownloadLogFragment().show(getSupportFragmentManager(), null);
}
if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) {
FeedUpdateManager.getInstance().runOnceOrAsk(this);
}
// to avoid handling the intent twice when the configuration changes
setIntent(new Intent(MainActivity.this, MainActivity.class));
}
@ -671,9 +761,7 @@ public class MainActivity extends CastEnabledActivity {
}
if (customKeyCode != null) {
Intent intent = new Intent(this, PlaybackService.class);
intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, customKeyCode);
ContextCompat.startForegroundService(this, intent);
sendBroadcast(MediaButtonStarter.createIntent(this, customKeyCode));
return true;
}
return super.onKeyUp(keyCode, event);

View File

@ -1,703 +0,0 @@
package de.danoeh.antennapod.activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.LightingColorFilter;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
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 android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException;
import de.danoeh.antennapod.core.util.DownloadErrorLabel;
import de.danoeh.antennapod.event.FeedListUpdateEvent;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
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.net.discovery.CombinedSearcher;
import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry;
import de.danoeh.antennapod.parser.feed.FeedHandler;
import de.danoeh.antennapod.parser.feed.FeedHandlerResult;
import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.URLChecker;
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.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.playback.RemoteMedia;
import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException;
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;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Downloads a feed from a feed URL and parses it. Subclasses can display the
* feed object that was parsed. This activity MUST be started with a given URL
* or an Exception will be thrown.
* <p/>
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
* and the activity will finish as soon as the error dialog is closed.
*/
public class OnlineFeedViewActivity extends AppCompatActivity {
public static final String ARG_FEEDURL = "arg.feedurl";
// Optional argument: specify a title for the actionbar.
private static final int RESULT_ERROR = 2;
private static final String TAG = "OnlineFeedViewActivity";
private static final String PREFS = "OnlineFeedViewActivityPreferences";
private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload";
private volatile List<Feed> feeds;
private String selectedDownloadUrl;
private Downloader downloader;
private String username = null;
private String password = null;
private boolean isPaused;
private boolean didPressSubscribe = false;
private boolean isFeedFoundBySearch = false;
private Dialog dialog;
private Disposable download;
private Disposable parser;
private Disposable updater;
private OnlinefeedviewActivityBinding viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTranslucentTheme());
super.onCreate(savedInstanceState);
viewBinding = OnlinefeedviewActivityBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
viewBinding.transparentBackground.setOnClickListener(v -> finish());
viewBinding.card.setOnClickListener(null);
String feedUrl = null;
if (getIntent().hasExtra(ARG_FEEDURL)) {
feedUrl = getIntent().getStringExtra(ARG_FEEDURL);
} else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) {
feedUrl = getIntent().getStringExtra(Intent.EXTRA_TEXT);
} else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
feedUrl = getIntent().getDataString();
}
if (feedUrl == null) {
Log.e(TAG, "feedUrl is null.");
showNoPodcastFoundError();
} else {
Log.d(TAG, "Activity was started with url " + feedUrl);
setLoadingLayout();
// Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL
if (feedUrl.contains("subscribeonandroid.com")) {
feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))", "");
}
if (savedInstanceState != null) {
username = savedInstanceState.getString("username");
password = savedInstanceState.getString("password");
}
lookupUrlAndDownload(feedUrl);
}
}
private void showNoPodcastFoundError() {
runOnUiThread(() -> new AlertDialog.Builder(OnlineFeedViewActivity.this)
.setNeutralButton(android.R.string.ok, (dialog, which) -> finish())
.setTitle(R.string.error_label)
.setMessage(R.string.null_value_podcast_error)
.setOnDismissListener(dialog1 -> {
setResult(RESULT_ERROR);
finish();
})
.show());
}
/**
* Displays a progress indicator.
*/
private void setLoadingLayout() {
viewBinding.progressBar.setVisibility(View.VISIBLE);
viewBinding.feedDisplayContainer.setVisibility(View.GONE);
}
@Override
protected void onStart() {
super.onStart();
isPaused = false;
EventBus.getDefault().register(this);
}
@Override
protected void onStop() {
super.onStop();
isPaused = true;
EventBus.getDefault().unregister(this);
if (downloader != null && !downloader.isFinished()) {
downloader.cancel();
}
if(dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if(updater != null) {
updater.dispose();
}
if(download != null) {
download.dispose();
}
if(parser != null) {
parser.dispose();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("username", username);
outState.putString("password", password);
}
private void resetIntent(String url) {
Intent intent = new Intent();
intent.putExtra(ARG_FEEDURL, url);
setIntent(intent);
}
@Override
public void finish() {
super.finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
Intent destIntent = new Intent(this, MainActivity.class);
if (NavUtils.shouldUpRecreateTask(this, destIntent)) {
startActivity(destIntent);
} else {
NavUtils.navigateUpFromSameTask(this);
}
return true;
}
return super.onOptionsItemSelected(item);
}
private void lookupUrlAndDownload(String url) {
download = PodcastSearcherRegistry.lookupUrl(url)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(this::startFeedDownload,
error -> {
if (error instanceof FeedUrlNotFoundException) {
tryToRetrieveFeedUrlBySearch((FeedUrlNotFoundException) error);
} else {
showNoPodcastFoundError();
Log.e(TAG, Log.getStackTraceString(error));
}
});
}
private void tryToRetrieveFeedUrlBySearch(FeedUrlNotFoundException error) {
Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search");
String url = searchFeedUrlByTrackName(error.getTrackName(), error.getArtistName());
if (url != null) {
Log.d(TAG, "Successfully retrieve feed url");
isFeedFoundBySearch = true;
startFeedDownload(url);
} else {
showNoPodcastFoundError();
Log.d(TAG, "Failed to retrieve feed url");
}
}
private String searchFeedUrlByTrackName(String trackName, String artistName) {
CombinedSearcher searcher = new CombinedSearcher();
String query = trackName + " " + artistName;
List<PodcastSearchResult> results = searcher.search(query).blockingGet();
for (PodcastSearchResult result : results) {
if (result.feedUrl != null && result.author != null
&& result.author.equalsIgnoreCase(artistName) && result.title.equalsIgnoreCase(trackName)) {
return result.feedUrl;
}
}
return null;
}
private void startFeedDownload(String url) {
Log.d(TAG, "Starting feed download");
selectedDownloadUrl = URLChecker.prepareURL(url);
DownloadRequest request = DownloadRequestCreator.create(new Feed(selectedDownloadUrl, null))
.withAuthentication(username, password)
.withInitiatedByUser(true)
.build();
download = Observable.fromCallable(() -> {
feeds = DBReader.getFeedList();
downloader = new HttpDownloader(request);
downloader.call();
return downloader.getResult();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(status -> checkDownloadResult(status, request.getDestination()),
error -> Log.e(TAG, Log.getStackTraceString(error)));
}
private void checkDownloadResult(@NonNull DownloadStatus status, String destination) {
if (status.isCancelled()) {
return;
}
if (status.isSuccessful()) {
parseFeed(destination);
} else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
if (!isFinishing() && !isPaused) {
if (username != null && password != null) {
Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show();
}
dialog = new FeedViewAuthenticationDialog(OnlineFeedViewActivity.this,
R.string.authentication_notification_title,
downloader.getDownloadRequest().getSource()).create();
dialog.show();
}
} else {
showErrorDialog(getString(DownloadErrorLabel.from(status.getReason())), status.getReasonDetailed());
}
}
@Subscribe
public void onFeedListChanged(FeedListUpdateEvent event) {
updater = Observable.fromCallable(DBReader::getFeedList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
feeds -> {
OnlineFeedViewActivity.this.feeds = feeds;
handleUpdatedFeedStatus();
}, error -> Log.e(TAG, Log.getStackTraceString(error))
);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
handleUpdatedFeedStatus();
}
private void parseFeed(String destination) {
Log.d(TAG, "Parsing feed");
parser = Maybe.fromCallable(() -> doParseFeed(destination))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.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));
}
});
}
/**
* 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(String destination) throws Exception {
FeedHandler handler = new FeedHandler();
Feed feed = new Feed(selectedDownloadUrl, null);
feed.setFile_url(destination);
File destinationFile = new File(destination);
try {
return handler.parseFeed(feed);
} catch (UnsupportedFeedtypeException e) {
Log.d(TAG, "Unsupported feed type detected");
if ("html".equalsIgnoreCase(e.getRootElement())) {
boolean dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl);
if (dialogShown) {
return null; // Should not display an error message
} else {
throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html));
}
} else {
throw e;
}
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
throw e;
} finally {
boolean rc = destinationFile.delete();
Log.d(TAG, "Deleted feed source file. Result: " + rc);
}
}
/**
* Called when feed parsed successfully.
* This method is executed on the GUI thread.
*/
private void showFeedInformation(final Feed feed, Map<String, String> alternateFeedUrls) {
viewBinding.progressBar.setVisibility(View.GONE);
viewBinding.feedDisplayContainer.setVisibility(View.VISIBLE);
if (isFeedFoundBySearch) {
int resId = R.string.no_feed_url_podcast_found_by_search;
Snackbar.make(findViewById(android.R.id.content), resId, Snackbar.LENGTH_LONG).show();
}
viewBinding.backgroundImage.setColorFilter(new LightingColorFilter(0xff828282, 0x000000));
View header = View.inflate(this, R.layout.onlinefeedview_header, null);
viewBinding.listView.addHeaderView(header);
viewBinding.listView.setSelector(android.R.color.transparent);
viewBinding.listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems()));
TextView description = header.findViewById(R.id.txtvDescription);
if (StringUtils.isNotBlank(feed.getImageUrl())) {
Glide.with(this)
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(viewBinding.coverImage);
Glide.with(this)
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transform(new FastBlurTransformation())
.dontAnimate())
.into(viewBinding.backgroundImage);
}
viewBinding.titleLabel.setText(feed.getTitle());
viewBinding.authorLabel.setText(feed.getAuthor());
description.setText(HtmlToPlainText.getPlainText(feed.getDescription()));
viewBinding.subscribeButton.setOnClickListener(v -> {
if (feedInFeedlist()) {
openFeed();
} else {
Feed f = new Feed(selectedDownloadUrl, null, feed.getTitle());
DownloadService.download(this, false, DownloadRequestCreator.create(f)
.withAuthentication(username, password)
.build());
didPressSubscribe = true;
handleUpdatedFeedStatus();
}
});
viewBinding.stopPreviewButton.setOnClickListener(v -> {
PlaybackPreferences.writeNoMediaPlaying();
IntentUtils.sendLocalBroadcast(this, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE);
});
if (UserPreferences.isEnableAutodownload()) {
SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE);
viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true));
}
final int MAX_LINES_COLLAPSED = 10;
description.setMaxLines(MAX_LINES_COLLAPSED);
description.setOnClickListener(v -> {
if (description.getMaxLines() > MAX_LINES_COLLAPSED) {
description.setMaxLines(MAX_LINES_COLLAPSED);
} else {
description.setMaxLines(2000);
}
});
if (alternateFeedUrls.isEmpty()) {
viewBinding.alternateUrlsSpinner.setVisibility(View.GONE);
} else {
viewBinding.alternateUrlsSpinner.setVisibility(View.VISIBLE);
final List<String> alternateUrlsList = new ArrayList<>();
final List<String> alternateUrlsTitleList = new ArrayList<>();
alternateUrlsList.add(feed.getDownload_url());
alternateUrlsTitleList.add(feed.getTitle());
alternateUrlsList.addAll(alternateFeedUrls.keySet());
for (String url : alternateFeedUrls.keySet()) {
alternateUrlsTitleList.add(alternateFeedUrls.get(url));
}
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
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedDownloadUrl = alternateUrlsList.get(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
handleUpdatedFeedStatus();
}
private void openFeed() {
// feed.getId() is always 0, we have to retrieve the id from the feed list from
// the database
Intent intent = MainActivity.getIntentToOpenFeed(this, getFeedId());
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH,
getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false));
finish();
startActivity(intent);
}
private void handleUpdatedFeedStatus() {
if (DownloadService.isDownloadingFile(selectedDownloadUrl)) {
viewBinding.subscribeButton.setEnabled(false);
viewBinding.subscribeButton.setText(R.string.subscribing_label);
} else if (feedInFeedlist()) {
viewBinding.subscribeButton.setEnabled(true);
viewBinding.subscribeButton.setText(R.string.open_podcast);
if (didPressSubscribe) {
didPressSubscribe = false;
if (UserPreferences.isEnableAutodownload()) {
boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked();
Feed feed1 = DBReader.getFeed(getFeedId());
FeedPreferences feedPreferences = feed1.getPreferences();
feedPreferences.setAutoDownload(autoDownload);
DBWriter.setFeedPreferences(feedPreferences);
SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload);
editor.apply();
}
openFeed();
}
} else {
viewBinding.subscribeButton.setEnabled(true);
viewBinding.subscribeButton.setText(R.string.subscribe_label);
if (UserPreferences.isEnableAutodownload()) {
viewBinding.autoDownloadCheckBox.setVisibility(View.VISIBLE);
}
}
}
private boolean feedInFeedlist() {
return getFeedId() != 0;
}
private long getFeedId() {
if (feeds == null) {
return 0;
}
for (Feed f : feeds) {
if (f.getDownload_url().equals(selectedDownloadUrl)) {
return f.getId();
}
}
return 0;
}
@UiThread
private void showErrorDialog(String errorMsg, String details) {
if (!isFinishing() && !isPaused) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.error_label);
if (errorMsg != null) {
String total = errorMsg + "\n\n" + details;
SpannableString errorMessage = new SpannableString(total);
errorMessage.setSpan(new ForegroundColorSpan(0x88888888),
errorMsg.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setMessage(errorMessage);
} else {
builder.setMessage(R.string.download_error_error_unknown);
}
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.cancel());
builder.setOnDismissListener(dialog -> {
setResult(RESULT_ERROR);
finish();
});
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
dialog = builder.show();
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void playbackStateChanged(PlayerStatusEvent event) {
boolean isPlayingPreview =
PlaybackPreferences.getCurrentlyPlayingMediaType() == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA;
viewBinding.stopPreviewButton.setVisibility(isPlayingPreview ? View.VISIBLE : View.GONE);
}
/**
*
* @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found).
*/
private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) {
FeedDiscoverer fd = new FeedDiscoverer();
final Map<String, String> urlsMap;
try {
urlsMap = fd.findLinks(feedFile, baseUrl);
if (urlsMap == null || urlsMap.isEmpty()) {
return false;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
if (isPaused || isFinishing()) {
return false;
}
final List<String> titles = new ArrayList<>();
final List<String> urls = new ArrayList<>(urlsMap.keySet());
for (String url : urls) {
titles.add(urlsMap.get(url));
}
if (urls.size() == 1) {
// Skip dialog and display the item directly
resetIntent(urls.get(0));
startFeedDownload(urls.get(0));
return true;
}
final ArrayAdapter<String> adapter = new ArrayAdapter<>(OnlineFeedViewActivity.this,
R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles);
DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
String selectedUrl = urls.get(which);
dialog.dismiss();
resetIntent(selectedUrl);
startFeedDownload(selectedUrl);
};
AlertDialog.Builder ab = new AlertDialog.Builder(OnlineFeedViewActivity.this)
.setTitle(R.string.feeds_label)
.setCancelable(true)
.setOnCancelListener(dialog -> finish())
.setAdapter(adapter, onClickListener);
runOnUiThread(() -> {
if(dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
dialog = ab.show();
});
return true;
}
private class FeedViewAuthenticationDialog extends AuthenticationDialog {
private final String feedUrl;
FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) {
super(context, titleRes, true, username, password);
this.feedUrl = feedUrl;
}
@Override
protected void onCancelled() {
super.onCancelled();
finish();
}
@Override
protected void onConfirmed(String username, String password) {
OnlineFeedViewActivity.this.username = username;
OnlineFeedViewActivity.this.password = password;
startFeedDownload(feedUrl);
}
}
}

View File

@ -6,7 +6,9 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.Menu;
@ -20,18 +22,18 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlReader;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import de.danoeh.antennapod.ui.common.ThemeSwitcher;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.storage.database.FeedDatabaseWriter;
import de.danoeh.antennapod.databinding.OpmlSelectionBinding;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.importexport.OpmlElement;
import de.danoeh.antennapod.storage.importexport.OpmlReader;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -43,7 +45,9 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Activity for Opml Import.
@ -51,7 +55,7 @@ import java.util.List;
public class OpmlImportActivity extends AppCompatActivity {
private static final String TAG = "OpmlImportBaseActivity";
@Nullable private Uri uri;
OpmlSelectionBinding viewBinding;
private OpmlSelectionBinding viewBinding;
private ArrayAdapter<String> listAdapter;
private MenuItem selectAll;
private MenuItem deselectAll;
@ -59,7 +63,7 @@ public class OpmlImportActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
setTheme(ThemeSwitcher.getTheme(this));
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
viewBinding = OpmlSelectionBinding.inflate(getLayoutInflater());
@ -95,9 +99,12 @@ public class OpmlImportActivity extends AppCompatActivity {
continue;
}
OpmlElement element = readElements.get(checked.keyAt(i));
Feed feed = new Feed(element.getXmlUrl(), null, element.getText());
DownloadService.download(this, false, DownloadRequestCreator.create(feed).build());
Feed feed = new Feed(element.getXmlUrl(), null,
element.getText() != null ? element.getText() : "Unknown podcast");
feed.setItems(Collections.emptyList());
FeedDatabaseWriter.updateFeed(this, feed, false);
}
FeedUpdateManager.getInstance().runOnce(this);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -109,6 +116,7 @@ public class OpmlImportActivity extends AppCompatActivity {
startActivity(intent);
finish();
}, e -> {
e.printStackTrace();
viewBinding.progressBar.setVisibility(View.GONE);
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
});
@ -128,22 +136,13 @@ public class OpmlImportActivity extends AppCompatActivity {
void importUri(@Nullable Uri uri) {
if (uri == null) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_error_no_file)
.setPositiveButton(android.R.string.ok, null)
.show();
return;
}
this.uri = uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& uri.toString().contains(Environment.getExternalStorageDirectory().toString())) {
int permission = ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
requestPermission();
return;
}
}
startImport();
}
@ -202,7 +201,7 @@ public class OpmlImportActivity extends AppCompatActivity {
if (isGranted) {
startImport();
} else {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_ask_read_permission)
.setPositiveButton(android.R.string.ok, (dialog, which) ->
requestPermission())
@ -239,12 +238,29 @@ public class OpmlImportActivity extends AppCompatActivity {
getTitleList());
viewBinding.feedlist.setAdapter(listAdapter);
}, e -> {
Log.d(TAG, Log.getStackTraceString(e));
String message = e.getMessage() == null ? "" : e.getMessage();
if (message.toLowerCase(Locale.ROOT).contains("permission")
&& Build.VERSION.SDK_INT >= 23) {
int permission = ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
requestPermission();
return;
}
}
viewBinding.progressBar.setVisibility(View.GONE);
AlertDialog.Builder alert = new AlertDialog.Builder(this);
MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(this);
alert.setTitle(R.string.error_label);
alert.setMessage(getString(R.string.opml_reader_error) + e.getMessage());
alert.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
alert.create().show();
String userReadable = getString(R.string.opml_reader_error);
String details = e.getMessage();
String total = userReadable + "\n\n" + details;
SpannableString errorMessage = new SpannableString(total);
errorMessage.setSpan(new ForegroundColorSpan(0x88888888),
userReadable.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
alert.setMessage(errorMessage);
alert.setPositiveButton(android.R.string.ok, (dialog, which) -> finish());
alert.show();
});
}
}

View File

@ -1,29 +0,0 @@
package de.danoeh.antennapod.activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.os.Bundle;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
public class PlaybackSpeedDialogActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTranslucentTheme());
super.onCreate(savedInstanceState);
VariableSpeedDialog speedDialog = new InnerVariableSpeedDialog();
speedDialog.show(getSupportFragmentManager(), null);
}
public static class InnerVariableSpeedDialog extends VariableSpeedDialog {
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
getActivity().finish();
}
}
}

View File

@ -1,168 +0,0 @@
package de.danoeh.antennapod.activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
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.ImportExportPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment;
/**
* PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see
* PreferenceController.
*/
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) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
final SettingsActivityBinding binding = SettingsActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG) == null) {
getSupportFragmentManager().beginTransaction()
.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) {
PreferenceFragmentCompat prefFragment = null;
if (screen == R.xml.preferences_user_interface) {
prefFragment = new UserInterfacePreferencesFragment();
} else if (screen == R.xml.preferences_network) {
prefFragment = new NetworkPreferencesFragment();
} else if (screen == R.xml.preferences_storage) {
prefFragment = new StoragePreferencesFragment();
} else if (screen == R.xml.preferences_import_export) {
prefFragment = new ImportExportPreferencesFragment();
} else if (screen == R.xml.preferences_autodownload) {
prefFragment = new AutoDownloadPreferencesFragment();
} else if (screen == R.xml.preferences_synchronization) {
prefFragment = new SynchronizationPreferencesFragment();
} else if (screen == R.xml.preferences_playback) {
prefFragment = new PlaybackPreferencesFragment();
} else if (screen == R.xml.preferences_notifications) {
prefFragment = new NotificationPreferencesFragment();
} else if (screen == R.xml.preferences_swipe) {
prefFragment = new SwipePreferencesFragment();
}
return prefFragment;
}
public static int getTitleOfPage(int preferences) {
if (preferences == R.xml.preferences_network) {
return R.string.network_pref;
} else if (preferences == R.xml.preferences_autodownload) {
return R.string.pref_automatic_download_title;
} else if (preferences == R.xml.preferences_playback) {
return R.string.playback_pref;
} else if (preferences == R.xml.preferences_storage) {
return R.string.storage_pref;
} else if (preferences == R.xml.preferences_import_export) {
return R.string.import_export_pref;
} else if (preferences == R.xml.preferences_user_interface) {
return R.string.user_interface_label;
} else if (preferences == R.xml.preferences_synchronization) {
return R.string.synchronization_pref;
} else if (preferences == R.xml.preferences_notifications) {
return R.string.notification_pref_fragment;
} else if (preferences == R.xml.feed_settings) {
return R.string.feed_settings_label;
} else if (preferences == R.xml.preferences_swipe) {
return R.string.swipeactions_label;
}
return R.string.settings_label;
}
public PreferenceFragmentCompat openScreen(int screen) {
PreferenceFragmentCompat fragment = getPreferenceScreen(screen);
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;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
finish();
} else {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
View view = getCurrentFocus();
//If no view currently has focus, create a new one, just so we can grab a window token from it
if (view == null) {
view = new View(this);
}
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
getSupportFragmentManager().popBackStack();
}
return true;
}
return false;
}
@Override
public void onSearchResultClicked(SearchPreferenceResult result) {
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);
}
}
}

View File

@ -26,11 +26,12 @@ import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.ui.common.ThemeSwitcher;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.databinding.SubscriptionSelectionActivityBinding;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -47,7 +48,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(UserPreferences.getTranslucentTheme());
setTheme(ThemeSwitcher.getTranslucentTheme(this));
super.onCreate(savedInstanceState);
viewBinding = SubscriptionSelectionActivityBinding.inflate(getLayoutInflater());
@ -77,7 +78,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
public List<Feed> getFeedItems(List<NavDrawerData.DrawerItem> items, List<Feed> result) {
for (NavDrawerData.DrawerItem item : items) {
if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
getFeedItems(((NavDrawerData.TagDrawerItem) item).children, result);
getFeedItems(((NavDrawerData.TagDrawerItem) item).getChildren(), result);
} else {
Feed feed = ((NavDrawerData.FeedDrawerItem) item).feed;
if (!result.contains(feed)) {
@ -99,7 +100,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
if (bitmap != null) {
icon = IconCompat.createWithAdaptiveBitmap(bitmap);
} else {
icon = IconCompat.createWithResource(this, R.drawable.ic_folder_shortcut);
icon = IconCompat.createWithResource(this, R.drawable.ic_shortcut_subscriptions);
}
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, id)
@ -142,7 +143,8 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
}
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData();
NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
return getFeedItems(data.items, new ArrayList<>());
})
.subscribeOn(Schedulers.io())

View File

@ -1,19 +1,13 @@
package de.danoeh.antennapod.activity;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.ProgressBar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.error.CrashReportWriter;
import de.danoeh.antennapod.CrashReportWriter;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -22,21 +16,13 @@ import io.reactivex.schedulers.Schedulers;
/**
* Shows the AntennaPod logo while waiting for the main activity to start.
*/
public class SplashActivity extends AppCompatActivity {
@SuppressLint("CustomSplashScreen")
public class SplashActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.splash);
ProgressBar progressBar = findViewById(R.id.progressBar);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Drawable wrapDrawable = DrawableCompat.wrap(progressBar.getIndeterminateDrawable());
DrawableCompat.setTint(wrapDrawable, 0xffffffff);
progressBar.setIndeterminateDrawable(DrawableCompat.unwrap(wrapDrawable));
} else {
progressBar.getIndeterminateDrawable().setColorFilter(
new PorterDuffColorFilter(0xffffffff, PorterDuff.Mode.SRC_IN));
}
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnPreDrawListener(() -> false); // Keep splash screen active
Completable.create(subscriber -> {
// Trigger schema updates

View File

@ -1,832 +0,0 @@
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.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.SeekBar;
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.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
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.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
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.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
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 CastEnabledActivity implements SeekBar.OnSeekBarChangeListener {
private static final String TAG = "VideoplayerActivity";
/**
* True if video controls are currently visible.
*/
private boolean videoControlsShowing = true;
private boolean videoSurfaceCreated = false;
private boolean destroyingDueToReload = false;
private long lastScreenTap = 0;
private Handler videoControlsHider = new Handler(Looper.getMainLooper());
private VideoplayerActivityBinding viewBinding;
private PlaybackController controller;
private boolean showTimeLeft = false;
private boolean isFavorite = false;
private boolean switchToAudioOnly = 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);
// 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()");
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();
switchToAudioOnly = false;
if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {
destroyingDueToReload = true;
finish();
startActivity(intent);
}
}
}
@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.removeCallbacks(hideVideoControls);
}
// Controller released; we will not receive buffering updates
viewBinding.progressBar.setVisibility(View.GONE);
}
@Override
public void onUserLeaveHint() {
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
compatEnterPictureInPicture();
}
}
@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) {
controller.pause();
}
}
super.onPause();
}
@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();
}
private PlaybackController newPlaybackController() {
return new PlaybackController(this) {
@Override
public void onPositionObserverUpdate() {
VideoplayerActivity.this.onPositionObserverUpdate();
}
@Override
public void onReloadNotification(int code) {
VideoplayerActivity.this.onReloadNotification(code);
}
@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);
}
}
};
}
@Subscribe(threadMode = ThreadMode.MAIN)
@SuppressWarnings("unused")
public void bufferUpdate(BufferUpdateEvent event) {
if (event.hasStarted()) {
viewBinding.progressBar.setVisibility(View.VISIBLE);
} else if (event.hasEnded()) {
viewBinding.progressBar.setVisibility(View.INVISIBLE);
} else {
viewBinding.sbPosition.setSecondaryProgress((int) (event.getProgress() * viewBinding.sbPosition.getMax()));
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
@SuppressWarnings("unused")
public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
if (event.isCancelled() || event.wasJustEnabled()) {
supportInvalidateOptionsMenu();
}
}
protected void 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());
getSupportActionBar().setTitle(media.getFeedTitle());
}
}
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);
viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched);
viewBinding.videoPlayerContainer.getViewTreeObserver().addOnGlobalLayoutListener(() ->
viewBinding.videoView.setAvailableSize(
viewBinding.videoPlayerContainer.getWidth(), viewBinding.videoPlayerContainer.getHeight()));
}
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) {
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;
}
toggleVideoControlsVisibility();
if (videoControlsShowing) {
setupVideoControlsToggler();
}
lastScreenTap = System.currentTimeMillis();
return true;
};
private void showSkipAnimation(boolean isForward) {
AnimationSet skipAnimation = new AnimationSet(true);
skipAnimation.addAnimation(new ScaleAnimation(1f, 2f, 1f, 2f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f));
skipAnimation.addAnimation(new AlphaAnimation(1f, 0f));
skipAnimation.setFillAfter(false);
skipAnimation.setDuration(800);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) viewBinding.skipAnimationImage.getLayoutParams();
if (isForward) {
viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white);
params.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
} else {
viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white);
params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
}
viewBinding.skipAnimationImage.setVisibility(View.VISIBLE);
viewBinding.skipAnimationImage.setLayoutParams(params);
viewBinding.skipAnimationImage.startAnimation(skipAnimation);
skipAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
viewBinding.skipAnimationImage.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
private void setupVideoControlsToggler() {
videoControlsHider.removeCallbacks(hideVideoControls);
videoControlsHider.postDelayed(hideVideoControls, 2500);
}
private void setupVideoAspectRatio() {
if (videoSurfaceCreated && controller != null) {
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);
viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second);
} else {
Log.e(TAG, "Could not determine video size");
}
}
}
private void toggleVideoControlsVisibility() {
if (videoControlsShowing) {
getSupportActionBar().hide();
hideVideoControls(true);
} else {
getSupportActionBar().show();
showVideoControls();
}
videoControlsShowing = !videoControlsShowing;
}
void onRewind() {
if (controller == null) {
return;
}
int curr = controller.getPosition();
controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000);
setupVideoControlsToggler();
}
void onPlayPause() {
if (controller == null) {
return;
}
controller.playPause();
setupVideoControlsToggler();
}
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) {
holder.setFixedSize(width, height);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "Videoview holder created");
videoSurfaceCreated = true;
if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) {
controller.setVideoSurface(holder);
}
setupVideoAspectRatio();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed");
videoSurfaceCreated = false;
if (controller != null && !destroyingDueToReload && !switchToAudioOnly) {
controller.notifyVideoSurfaceAbandoned();
}
}
};
protected void onReloadNotification(int notificationCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO
|| notificationCode == PlaybackService.EXTRA_CODE_CAST) {
finish();
}
return;
}
if (notificationCode == PlaybackService.EXTRA_CODE_CAST) {
Log.d(TAG, "ReloadNotification received, switching to Castplayer now");
destroyingDueToReload = true;
finish();
new MainActivityStarter(this).withOpenPlayer().start();
}
}
private void showVideoControls() {
viewBinding.bottomControlsContainer.setVisibility(View.VISIBLE);
viewBinding.controlsContainer.setVisibility(View.VISIBLE);
final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in);
if (animation != null) {
viewBinding.bottomControlsContainer.startAnimation(animation);
viewBinding.controlsContainer.startAnimation(animation);
}
viewBinding.videoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
private void hideVideoControls(boolean showAnimation) {
if (showAnimation) {
final Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out);
if (animation != null) {
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);
viewBinding.bottomControlsContainer.setFitsSystemWindows(true);
viewBinding.bottomControlsContainer.setVisibility(View.GONE);
viewBinding.controlsContainer.setVisibility(View.GONE);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
onPositionObserverUpdate();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onPlaybackServiceChanged(PlaybackServiceEvent event) {
if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
finish();
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMediaPlayerError(PlayerErrorEvent event) {
final AlertDialog.Builder errorDialog = new AlertDialog.Builder(VideoplayerActivity.this);
errorDialog.setTitle(R.string.error_label);
errorDialog.setMessage(event.getMessage());
errorDialog.setNeutralButton(android.R.string.ok, (dialog, which) -> finish());
errorDialog.show();
}
@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());
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true);
menu.findItem(R.id.audio_controls).setIcon(R.drawable.ic_sliders);
menu.findItem(R.id.playback_speed).setVisible(true);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.player_switch_to_audio_only) {
switchToAudioOnly = true;
finish();
return true;
}
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 if (item.getItemId() == R.id.playback_speed) {
new VariableSpeedDialog().show(getSupportFragmentManager(), null);
} else {
return false;
}
return true;
}
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;
}
void onPositionObserverUpdate() {
if (controller == 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;
}
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 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()));
}
@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));
}
}
@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);
}
@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();
}
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
public boolean onKeyUp(int keyCode, KeyEvent event) {
View currentFocus = getCurrentFocus();
if (currentFocus instanceof EditText) {
return super.onKeyUp(keyCode, event);
}
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
switch (keyCode) {
case KeyEvent.KEYCODE_P: //Fallthrough
case KeyEvent.KEYCODE_SPACE:
onPlayPause();
toggleVideoControlsVisibility();
return true;
case KeyEvent.KEYCODE_J: //Fallthrough
case KeyEvent.KEYCODE_A:
case KeyEvent.KEYCODE_COMMA:
onRewind();
showSkipAnimation(false);
return true;
case KeyEvent.KEYCODE_K: //Fallthrough
case KeyEvent.KEYCODE_D:
case KeyEvent.KEYCODE_PERIOD:
onFastForward();
showSkipAnimation(true);
return true;
case KeyEvent.KEYCODE_F: //Fallthrough
case KeyEvent.KEYCODE_ESCAPE:
//Exit fullscreen mode
onBackPressed();
return true;
case KeyEvent.KEYCODE_I:
compatEnterPictureInPicture();
return true;
case KeyEvent.KEYCODE_PLUS: //Fallthrough
case KeyEvent.KEYCODE_W:
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
return true;
case KeyEvent.KEYCODE_MINUS: //Fallthrough
case KeyEvent.KEYCODE_S:
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI);
return true;
case KeyEvent.KEYCODE_M:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI);
return true;
}
break;
}
//Go to x% of video:
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
controller.seekTo((int) (0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller.getDuration()));
return true;
}
return super.onKeyUp(keyCode, event);
}
}

View File

@ -1,125 +0,0 @@
package de.danoeh.antennapod.activity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
import de.danoeh.antennapod.core.widget.WidgetUpdaterWorker;
public class WidgetConfigActivity extends AppCompatActivity {
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
private SeekBar opacitySeekBar;
private TextView opacityTextView;
private View widgetPreview;
private CheckBox ckPlaybackSpeed;
private CheckBox ckRewind;
private CheckBox ckFastForward;
private CheckBox ckSkip;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_widget_config);
Intent configIntent = getIntent();
Bundle extras = configIntent.getExtras();
if (extras != null) {
appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_CANCELED, resultValue);
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
opacityTextView = findViewById(R.id.widget_opacity_textView);
opacitySeekBar = findViewById(R.id.widget_opacity_seekBar);
widgetPreview = findViewById(R.id.widgetLayout);
findViewById(R.id.butConfirm).setOnClickListener(v -> confirmCreateWidget());
opacitySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
opacityTextView.setText(seekBar.getProgress() + "%");
int color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
widgetPreview.setBackgroundColor(color);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
widgetPreview.findViewById(R.id.txtNoPlaying).setVisibility(View.GONE);
TextView title = widgetPreview.findViewById(R.id.txtvTitle);
title.setVisibility(View.VISIBLE);
title.setText(R.string.app_name);
TextView progress = widgetPreview.findViewById(R.id.txtvProgress);
progress.setVisibility(View.VISIBLE);
progress.setText(R.string.position_default_label);
ckPlaybackSpeed = findViewById(R.id.ckPlaybackSpeed);
ckPlaybackSpeed.setOnClickListener(v -> displayPreviewPanel());
ckRewind = findViewById(R.id.ckRewind);
ckRewind.setOnClickListener(v -> displayPreviewPanel());
ckFastForward = findViewById(R.id.ckFastForward);
ckFastForward.setOnClickListener(v -> displayPreviewPanel());
ckSkip = findViewById(R.id.ckSkip);
ckSkip.setOnClickListener(v -> displayPreviewPanel());
}
private void displayPreviewPanel() {
boolean showExtendedPreview =
ckPlaybackSpeed.isChecked() || ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked();
widgetPreview.findViewById(R.id.extendedButtonsContainer)
.setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE);
widgetPreview.findViewById(R.id.butPlaybackSpeed)
.setVisibility(ckPlaybackSpeed.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butFastForward)
.setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE);
widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE);
}
private void confirmCreateWidget() {
int backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor);
editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked());
editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked());
editor.apply();
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, resultValue);
finish();
WidgetUpdaterWorker.enqueueWork(this);
}
private int getColorWithAlpha(int color, int opacity) {
return (int) Math.round(0xFF * (0.01 * opacity)) * 0x1000000 + color;
}
}

View File

@ -1,179 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
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.R;
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.model.feed.EmbeddedChapterImage;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.ui.common.CircularProgressBar;
public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder> {
private Playable media;
private final Callback callback;
private final Context context;
private int currentChapterIndex = -1;
private long currentChapterPosition = -1;
private boolean hasImages = false;
public ChaptersListAdapter(Context context, Callback callback) {
this.callback = callback;
this.context = context;
}
public void setMedia(Playable media) {
this.media = media;
hasImages = false;
if (media.getChapters() != null) {
for (Chapter chapter : media.getChapters()) {
if (!TextUtils.isEmpty(chapter.getImageUrl())) {
hasImages = true;
}
}
}
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(@NonNull ChapterHolder holder, int position) {
Chapter sc = getItem(position);
if (sc == null) {
holder.title.setText("Error");
return;
}
holder.title.setText(sc.getTitle());
holder.start.setText(Converter.getDurationStringLong((int) sc
.getStart()));
long duration;
if (position + 1 < media.getChapters().size()) {
duration = media.getChapters().get(position + 1).getStart() - sc.getStart();
} else {
duration = media.getDuration() - sc.getStart();
}
holder.duration.setText(context.getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(context, (int) duration)));
if (TextUtils.isEmpty(sc.getLink())) {
holder.link.setVisibility(View.GONE);
} else {
holder.link.setVisibility(View.VISIBLE);
holder.link.setText(sc.getLink());
holder.link.setOnClickListener(v -> IntentUtils.openInBrowser(context, sc.getLink()));
}
holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp);
holder.secondaryActionButton.setContentDescription(context.getString(R.string.play_chapter));
holder.secondaryActionButton.setOnClickListener(v -> {
if (callback != null) {
callback.onPlayChapterButtonClicked(position);
}
});
if (position == currentChapterIndex) {
int playingBackGroundColor = ThemeUtils.getColorFromAttr(context, R.attr.currently_playing_background);
holder.itemView.setBackgroundColor(playingBackGroundColor);
float progress = ((float) (currentChapterPosition - sc.getStart())) / duration;
progress = Math.max(progress, CircularProgressBar.MINIMUM_PERCENTAGE);
progress = Math.min(progress, CircularProgressBar.MAXIMUM_PERCENTAGE);
holder.progressBar.setPercentage(progress, position);
holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay);
} else {
holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent));
holder.progressBar.setPercentage(0, null);
}
if (hasImages) {
holder.image.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(sc.getImageUrl())) {
Glide.with(context).clear(holder.image);
} else {
Glide.with(context)
.load(EmbeddedChapterImage.getModelFor(media, position))
.apply(new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transform(new FitCenter(), new RoundedCorners((int)
(4 * context.getResources().getDisplayMetrics().density))))
.into(holder.image);
}
} else {
holder.image.setVisibility(View.GONE);
}
}
@NonNull
@Override
public ChapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
return new ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false));
}
@Override
public int getItemCount() {
if (media == null || media.getChapters() == null) {
return 0;
}
return media.getChapters().size();
}
static class ChapterHolder extends RecyclerView.ViewHolder {
final TextView title;
final TextView start;
final TextView link;
final TextView duration;
final ImageView image;
final View secondaryActionButton;
final ImageView secondaryActionIcon;
final CircularProgressBar progressBar;
public ChapterHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.txtvTitle);
start = itemView.findViewById(R.id.txtvStart);
link = itemView.findViewById(R.id.txtvLink);
image = itemView.findViewById(R.id.imgvCover);
duration = itemView.findViewById(R.id.txtvDuration);
secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton);
secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon);
progressBar = itemView.findViewById(R.id.secondaryActionProgress);
}
}
public void notifyChapterChanged(int newChapterIndex) {
currentChapterIndex = newChapterIndex;
currentChapterPosition = getItem(newChapterIndex).getStart();
notifyDataSetChanged();
}
public void notifyTimeChanged(long timeMs) {
currentChapterPosition = timeMs;
// Passing an argument prevents flickering.
// See EpisodeItemListAdapter.notifyItemChangedCompat.
notifyItemChanged(currentChapterIndex, "foo");
}
public Chapter getItem(int position) {
return media.getChapters().get(position);
}
public interface Callback {
void onPlayChapterButtonClicked(int position);
}
}

View File

@ -1,173 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.palette.graphics.Palette;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomViewTarget;
import java.lang.ref.WeakReference;
import com.bumptech.glide.request.transition.Transition;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.PaletteBitmap;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.ui.common.ThemeUtils;
public class CoverLoader {
private int resource = 0;
private String uri;
private String fallbackUri;
private TextView txtvPlaceholder;
private ImageView imgvCover;
private boolean textAndImageCombined;
private MainActivity activity;
public CoverLoader(MainActivity activity) {
this.activity = activity;
}
public CoverLoader withUri(String uri) {
this.uri = uri;
return this;
}
public CoverLoader withResource(int resource) {
this.resource = resource;
return this;
}
public CoverLoader withFallbackUri(String uri) {
fallbackUri = uri;
return this;
}
public CoverLoader withCoverView(ImageView coverView) {
imgvCover = coverView;
return this;
}
public CoverLoader withPlaceholderView(TextView placeholderView) {
txtvPlaceholder = placeholderView;
return this;
}
/**
* Set cover text and if it should be shown even if there is a cover image.
*
* @param placeholderView Cover text.
* @param textAndImageCombined Show cover text even if there is a cover image?
*/
@NonNull
public CoverLoader withPlaceholderView(@NonNull TextView placeholderView, boolean textAndImageCombined) {
this.txtvPlaceholder = placeholderView;
this.textAndImageCombined = textAndImageCombined;
return this;
}
public void load() {
CoverTarget coverTarget = new CoverTarget(txtvPlaceholder, imgvCover, textAndImageCombined);
if (resource != 0) {
Glide.with(activity).clear(coverTarget);
imgvCover.setImageResource(resource);
CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined, null);
return;
}
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate();
RequestBuilder<PaletteBitmap> builder = Glide.with(activity)
.as(PaletteBitmap.class)
.load(uri)
.apply(options);
if (fallbackUri != null && txtvPlaceholder != null && imgvCover != null) {
builder = builder.error(Glide.with(activity)
.as(PaletteBitmap.class)
.load(fallbackUri)
.apply(options));
}
builder.into(coverTarget);
}
static class CoverTarget extends CustomViewTarget<ImageView, PaletteBitmap> {
private final WeakReference<TextView> placeholder;
private final WeakReference<ImageView> cover;
private boolean textAndImageCombined;
public CoverTarget(TextView txtvPlaceholder, ImageView imgvCover, boolean textAndImageCombined) {
super(imgvCover);
if (txtvPlaceholder != null) {
txtvPlaceholder.setVisibility(View.VISIBLE);
}
placeholder = new WeakReference<>(txtvPlaceholder);
cover = new WeakReference<>(imgvCover);
this.textAndImageCombined = textAndImageCombined;
}
@Override
public void onLoadFailed(Drawable errorDrawable) {
setPlaceholderVisibility(this.placeholder.get(), true, null);
}
@Override
public void onResourceReady(@NonNull PaletteBitmap resource,
@Nullable Transition<? super PaletteBitmap> transition) {
ImageView ivCover = cover.get();
ivCover.setImageBitmap(resource.bitmap);
setPlaceholderVisibility(placeholder.get(), textAndImageCombined, resource.palette);
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
ImageView ivCover = cover.get();
ivCover.setImageDrawable(placeholder);
setPlaceholderVisibility(this.placeholder.get(), textAndImageCombined, null);
}
static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined, Palette palette) {
boolean showTitle = UserPreferences.shouldShowSubscriptionTitle();
if (placeholder != null) {
if (textAndImageCombined || showTitle) {
final Context context = placeholder.getContext();
placeholder.setVisibility(View.VISIBLE);
int bgColor = ContextCompat.getColor(context, R.color.feed_text_bg);
if (palette == null || !showTitle) {
placeholder.setBackgroundColor(bgColor);
placeholder.setTextColor(ThemeUtils.getColorFromAttr(placeholder.getContext(),
android.R.attr.textColorPrimary));
return;
}
int dominantColor = palette.getDominantColor(bgColor);
int textColor = ContextCompat.getColor(context, R.color.white);
if (ColorUtils.calculateLuminance(dominantColor) > 0.5) {
textColor = ContextCompat.getColor(context, R.color.black);
}
placeholder.setTextColor(textColor);
placeholder.setBackgroundColor(dominantColor);
} else {
placeholder.setVisibility(View.INVISIBLE);
}
}
}
}
}

View File

@ -1,139 +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.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.StorageUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class DataFolderAdapter extends RecyclerView.Adapter<DataFolderAdapter.ViewHolder> {
private final Consumer<String> selectionHandler;
private final String currentPath;
private final List<StoragePath> entries;
private final String freeSpaceString;
public DataFolderAdapter(Context context, @NonNull Consumer<String> selectionHandler) {
this.entries = getStorageEntries(context);
this.currentPath = getCurrentPath();
this.selectionHandler = selectionHandler;
this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false);
return new ViewHolder(entryView);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
StoragePath storagePath = entries.get(position);
Context context = holder.root.getContext();
String freeSpace = Formatter.formatShortFileSize(context, storagePath.getAvailableSpace());
String totalSpace = Formatter.formatShortFileSize(context, storagePath.getTotalSpace());
holder.path.setText(storagePath.getShortPath());
holder.size.setText(String.format(freeSpaceString, freeSpace, totalSpace));
holder.progressBar.setProgress(storagePath.getUsagePercentage());
View.OnClickListener selectListener = v -> selectionHandler.accept(storagePath.getFullPath());
holder.root.setOnClickListener(selectListener);
holder.radioButton.setOnClickListener(selectListener);
if (storagePath.getFullPath().equals(currentPath)) {
holder.radioButton.toggle();
}
}
@Override
public int getItemCount() {
return entries.size();
}
private String getCurrentPath() {
File dataFolder = UserPreferences.getDataFolder(null);
if (dataFolder != null) {
return dataFolder.getAbsolutePath();
}
return null;
}
private List<StoragePath> getStorageEntries(Context context) {
File[] mediaDirs = context.getExternalFilesDirs(null);
final List<StoragePath> entries = new ArrayList<>(mediaDirs.length);
for (File dir : mediaDirs) {
if (!isWritable(dir)) {
continue;
}
entries.add(new StoragePath(dir.getAbsolutePath()));
}
if (entries.isEmpty() && isWritable(context.getFilesDir())) {
entries.add(new StoragePath(context.getFilesDir().getAbsolutePath()));
}
return entries;
}
private boolean isWritable(File dir) {
return dir != null && dir.exists() && dir.canRead() && dir.canWrite();
}
static class ViewHolder extends RecyclerView.ViewHolder {
private final View root;
private final TextView path;
private final TextView size;
private final RadioButton radioButton;
private final ProgressBar progressBar;
ViewHolder(View itemView) {
super(itemView);
root = itemView.findViewById(R.id.root);
path = itemView.findViewById(R.id.path);
size = itemView.findViewById(R.id.size);
radioButton = itemView.findViewById(R.id.radio_button);
progressBar = itemView.findViewById(R.id.used_space);
}
}
static class StoragePath {
private final String path;
StoragePath(String path) {
this.path = path;
}
String getShortPath() {
int prefixIndex = path.indexOf("Android");
return (prefixIndex > 0) ? path.substring(0, prefixIndex) : path;
}
String getFullPath() {
return this.path;
}
long getAvailableSpace() {
return StorageUtils.getFreeSpaceAvailable(path);
}
long getTotalSpace() {
return StorageUtils.getTotalSpaceAvailable(path);
}
int getUsagePercentage() {
return 100 - (int) (100 * getAvailableSpace() / (float) getTotalSpace());
}
}
}

View File

@ -1,228 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
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.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.DownloadErrorLabel;
import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.DownloadLogItemViewHolder;
import java.util.ArrayList;
import java.util.List;
/**
* Displays a list of DownloadStatus entries.
*/
public class DownloadLogAdapter extends BaseAdapter {
private static final String TAG = "DownloadLogAdapter";
private final Activity context;
private List<DownloadStatus> downloadLog = new ArrayList<>();
private List<Downloader> runningDownloads = new ArrayList<>();
public DownloadLogAdapter(Activity context) {
super();
this.context = context;
}
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) {
DownloadLogItemViewHolder holder;
if (convertView == null) {
holder = new DownloadLogItemViewHolder(context, parent);
holder.itemView.setTag(holder);
} else {
holder = (DownloadLogItemViewHolder) convertView.getTag();
}
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) {
statusText += context.getString(R.string.download_type_feed);
} else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
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);
}
if (status.isSuccessful()) {
holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_success_green));
holder.icon.setText("{fa-check-circle}");
holder.icon.setContentDescription(context.getString(R.string.download_successful));
holder.secondaryActionButton.setVisibility(View.INVISIBLE);
holder.reason.setVisibility(View.GONE);
holder.tapForDetails.setVisibility(View.GONE);
} else {
if (status.getReason() == DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE) {
holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_warning_yellow));
holder.icon.setText("{fa-exclamation-circle}");
} else {
holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_failed_red));
holder.icon.setText("{fa-times-circle}");
}
holder.icon.setContentDescription(context.getString(R.string.error_label));
holder.reason.setText(DownloadErrorLabel.from(status.getReason()));
holder.reason.setVisibility(View.VISIBLE);
holder.tapForDetails.setVisibility(View.VISIBLE);
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(R.drawable.ic_refresh);
holder.secondaryActionButton.setVisibility(View.VISIBLE);
if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
holder.secondaryActionButton.setOnClickListener(v -> {
holder.secondaryActionButton.setVisibility(View.INVISIBLE);
Feed feed = DBReader.getFeed(status.getFeedfileId());
if (feed == null) {
Log.e(TAG, "Could not find feed for feed id: " + status.getFeedfileId());
return;
}
DBTasks.forceRefreshFeed(context, feed, true);
});
} else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
holder.secondaryActionButton.setOnClickListener(v -> {
holder.secondaryActionButton.setVisibility(View.INVISIBLE);
FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId());
if (media == null) {
Log.e(TAG, "Could not find feed media for feed id: " + status.getFeedfileId());
return;
}
DownloadService.download(context, true, DownloadRequestCreator.create(media).build());
((MainActivity) context).showSnackbarAbovePlayer(
R.string.status_downloading_label, Toast.LENGTH_SHORT);
});
}
}
}
}
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 -> {
DownloadService.cancel(context, request.getSource());
if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId());
FeedItem feedItem = media.getItem();
feedItem.disableAutoDownload();
DBWriter.setFeedItem(feedItem);
}
});
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 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;
}
}
return false;
}
@Override
public int getCount() {
return downloadLog.size() + runningDownloads.size();
}
@Override
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
public long getItemId(int position) {
return position;
}
}

View File

@ -1,215 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.os.Build;
import android.view.ContextMenu;
import android.view.InputDevice;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
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;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
/**
* List adapter for the list of new episodes.
*/
public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<FeedItem> episodes = new ArrayList<>();
private FeedItem longPressedItem;
int longPressedPosition = 0; // used to init actionMode
public EpisodeItemListAdapter(MainActivity mainActivity) {
super(mainActivity);
this.mainActivityRef = new WeakReference<>(mainActivity);
setHasStableIds(true);
}
public void updateItems(List<FeedItem> items) {
episodes = items;
notifyDataSetChanged();
updateTitle();
}
@Override
public final int getItemViewType(int position) {
return R.id.view_type_episode_item;
}
@NonNull
@Override
public final EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new EpisodeItemViewHolder(mainActivityRef.get(), parent);
}
@Override
public final void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
// Reset state of recycled views
holder.coverHolder.setVisibility(View.VISIBLE);
holder.dragHandle.setVisibility(View.GONE);
beforeBindViewHolder(holder, pos);
FeedItem item = episodes.get(pos);
holder.bind(item);
holder.itemView.setOnClickListener(v -> {
MainActivity activity = mainActivityRef.get();
if (activity != null && !inActionMode()) {
long[] ids = FeedItemUtil.getIds(episodes);
int position = ArrayUtils.indexOf(ids, item.getId());
activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position));
} else {
toggleSelection(holder.getBindingAdapterPosition());
}
});
holder.itemView.setOnCreateContextMenuListener(this);
holder.itemView.setOnLongClickListener(v -> {
longPressedItem = item;
longPressedPosition = holder.getBindingAdapterPosition();
return false;
});
holder.itemView.setOnTouchListener((v, e) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
longPressedItem = item;
longPressedPosition = holder.getBindingAdapterPosition();
return false;
}
}
return false;
});
if (inActionMode()) {
holder.secondaryActionButton.setVisibility(View.GONE);
holder.selectCheckBox.setOnClickListener(v -> toggleSelection(holder.getBindingAdapterPosition()));
holder.selectCheckBox.setChecked(isSelected(pos));
holder.selectCheckBox.setVisibility(View.VISIBLE);
} else {
holder.selectCheckBox.setVisibility(View.GONE);
}
afterBindViewHolder(holder, pos);
holder.hideSeparatorIfNecessary();
}
protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) {
}
protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) {
}
@Override
public void onViewRecycled(@NonNull EpisodeItemViewHolder holder) {
super.onViewRecycled(holder);
// Set all listeners to null. This is required to prevent leaking fragments that have set a listener.
// Activity -> recycledViewPool -> EpisodeItemViewHolder -> Listener -> Fragment (can not be garbage collected)
holder.itemView.setOnClickListener(null);
holder.itemView.setOnCreateContextMenuListener(null);
holder.itemView.setOnLongClickListener(null);
holder.itemView.setOnTouchListener(null);
holder.secondaryActionButton.setOnClickListener(null);
holder.dragHandle.setOnTouchListener(null);
holder.coverHolder.setOnTouchListener(null);
}
/**
* {@link #notifyItemChanged(int)} is final, so we can not override.
* Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition.
* This causes flickering and breaks the download animation that stores the old progress in the View.
* Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}.
* We actually ignore the payload and always do a full bind but calling the partial bind method ensures
* that ViewHolders are always re-used.
*
* @param position Position of the item that has changed
*/
public void notifyItemChangedCompat(int position) {
notifyItemChanged(position, "foo");
}
@Nullable
public FeedItem getLongPressedItem() {
return longPressedItem;
}
@Override
public long getItemId(int position) {
FeedItem item = episodes.get(position);
return item != null ? item.getId() : RecyclerView.NO_POSITION;
}
@Override
public int getItemCount() {
return episodes.size();
}
protected FeedItem getItem(int index) {
return episodes.get(index);
}
protected Activity getActivity() {
return mainActivityRef.get();
}
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
if (inActionMode()) {
inflater.inflate(R.menu.multi_select_context_popup, menu);
} else {
if (longPressedItem == null) {
return;
}
inflater.inflate(R.menu.feeditemlist_context, menu);
menu.setHeaderTitle(longPressedItem.getTitle());
FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item);
}
}
public boolean onContextItemSelected(MenuItem item) {
if (item.getItemId() == R.id.multi_select) {
startSelectMode(longPressedPosition);
return true;
} else if (item.getItemId() == R.id.select_all_above) {
setSelected(0, longPressedPosition, true);
return true;
} else if (item.getItemId() == R.id.select_all_below) {
shouldSelectLazyLoadedItems = true;
setSelected(longPressedPosition + 1, getItemCount(), true);
return true;
}
return false;
}
public List<FeedItem> getSelectedItems() {
List<FeedItem> items = new ArrayList<>();
for (int i = 0; i < getItemCount(); i++) {
if (isSelected(i)) {
items.add(getItem(i));
}
}
return items;
}
}

View File

@ -1,78 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
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.net.discovery.PodcastSearchResult;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class FeedDiscoverAdapter extends BaseAdapter {
private final WeakReference<MainActivity> mainActivityRef;
private final List<PodcastSearchResult> data = new ArrayList<>();
public FeedDiscoverAdapter(MainActivity mainActivity) {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
public void updateData(List<PodcastSearchResult> newData) {
data.clear();
data.addAll(newData);
notifyDataSetChanged();
}
@Override
public int getCount() {
return data.size();
}
@Override
public PodcastSearchResult getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder;
if (convertView == null) {
convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null);
holder = new Holder();
holder.imageView = convertView.findViewById(R.id.discovery_cover);
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
}
final PodcastSearchResult podcast = getItem(position);
holder.imageView.setContentDescription(podcast.title);
Glide.with(mainActivityRef.get())
.load(podcast.imageUrl)
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(holder.imageView);
return convertView;
}
static class Holder {
ImageView imageView;
}
}

View File

@ -1,112 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.util.NetworkUtils;
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.DateFormatter;
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;
/**
* List adapter for showing a list of FeedItems with their title and description.
*/
public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
private static final int MAX_LINES_COLLAPSED = 3;
public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) {
super(context, resource, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Holder holder;
FeedItem item = getItem(position);
// Inflate layout
if (convertView == null) {
holder = new Holder();
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false);
holder.title = convertView.findViewById(R.id.txtvTitle);
holder.pubDate = convertView.findViewById(R.id.txtvPubDate);
holder.description = convertView.findViewById(R.id.txtvDescription);
holder.preview = convertView.findViewById(R.id.butPreview);
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
}
holder.title.setText(item.getTitle());
holder.pubDate.setText(DateFormatter.formatAbbrev(getContext(), item.getPubDate()));
if (item.getDescription() != null) {
String description = HtmlToPlainText.getPlainText(item.getDescription())
.replaceAll("\n", " ")
.replaceAll("\\s+", " ")
.trim();
holder.description.setText(description);
holder.description.setMaxLines(MAX_LINES_COLLAPSED);
}
holder.description.setTag(Boolean.FALSE); // not expanded
holder.preview.setVisibility(View.GONE);
holder.preview.setOnClickListener(v -> {
if (item.getMedia() == null) {
return;
}
Playable playable = new RemoteMedia(item);
if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(getContext(), playable).show();
return;
}
new PlaybackServiceStarter(getContext(), playable)
.callEvenIfRunning(true)
.start();
if (playable.getMediaType() == MediaType.VIDEO) {
getContext().startActivity(PlaybackService.getPlayerActivityIntent(getContext(), playable));
}
});
convertView.setOnClickListener(v -> {
if (holder.description.getTag() == Boolean.TRUE) {
holder.description.setMaxLines(MAX_LINES_COLLAPSED);
holder.preview.setVisibility(View.GONE);
holder.description.setTag(Boolean.FALSE);
} else {
holder.description.setMaxLines(30);
holder.description.setTag(Boolean.TRUE);
holder.preview.setVisibility(item.getMedia() != null ? View.VISIBLE : View.GONE);
holder.preview.setText(R.string.preview_episode);
}
});
return convertView;
}
static class Holder {
TextView title;
TextView pubDate;
TextView description;
Button preview;
}
}

View File

@ -1,76 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
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.model.feed.Feed;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.ui.common.SquareImageView;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResultAdapter.Holder> {
private final WeakReference<MainActivity> mainActivityRef;
private final List<Feed> data = new ArrayList<>();
public FeedSearchResultAdapter(MainActivity mainActivity) {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
public void updateData(List<Feed> newData) {
data.clear();
data.addAll(newData);
notifyDataSetChanged();
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View convertView = View.inflate(mainActivityRef.get(), R.layout.searchlist_item_feed, null);
return new Holder(convertView);
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
final Feed podcast = data.get(position);
holder.imageView.setContentDescription(podcast.getTitle());
holder.imageView.setOnClickListener(v ->
mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId())));
Glide.with(mainActivityRef.get())
.load(podcast.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(holder.imageView);
}
@Override
public long getItemId(int position) {
return data.get(position).getId();
}
@Override
public int getItemCount() {
return data.size();
}
static class Holder extends RecyclerView.ViewHolder {
SquareImageView imageView;
public Holder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.discovery_cover);
imageView.setDirection(SquareImageView.DIRECTION_HEIGHT);
}
}
}

View File

@ -1,427 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.view.ContextMenu;
import android.view.InputDevice;
import android.view.LayoutInflater;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
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 de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.fragment.CompletedDownloadsFragment;
import de.danoeh.antennapod.fragment.InboxFragment;
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.NavDrawerFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* BaseAdapter for the navigation drawer
*/
public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
implements SharedPreferences.OnSharedPreferenceChangeListener {
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;
/**
* a tag used as a placeholder to indicate if the subscription list should be displayed or not
* This tag doesn't correspond to any specific activity.
*/
public static final String SUBSCRIPTION_LIST_TAG = "SubscriptionList";
private final List<String> fragmentTags = new ArrayList<>();
private final String[] titles;
private final ItemAccess itemAccess;
private final WeakReference<Activity> activity;
public boolean showSubscriptionList = true;
public NavListAdapter(ItemAccess itemAccess, Activity context) {
this.itemAccess = itemAccess;
this.activity = new WeakReference<>(context);
titles = context.getResources().getStringArray(R.array.nav_drawer_titles);
loadItems();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.registerOnSharedPreferenceChangeListener(this);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) {
loadItems();
}
}
private void loadItems() {
List<String> newTags = new ArrayList<>(Arrays.asList(NavDrawerFragment.NAV_DRAWER_TAGS));
List<String> hiddenFragments = UserPreferences.getHiddenDrawerItems();
newTags.removeAll(hiddenFragments);
if (newTags.contains(SUBSCRIPTION_LIST_TAG)) {
// we never want SUBSCRIPTION_LIST_TAG to be in 'tags'
// since it doesn't actually correspond to a position in the list, but is
// a placeholder that indicates if we should show the subscription list in the
// nav drawer at all.
showSubscriptionList = true;
newTags.remove(SUBSCRIPTION_LIST_TAG);
} else {
showSubscriptionList = false;
}
fragmentTags.clear();
fragmentTags.addAll(newTags);
notifyDataSetChanged();
}
public String getLabel(String tag) {
int index = ArrayUtils.indexOf(NavDrawerFragment.NAV_DRAWER_TAGS, tag);
return titles[index];
}
private @DrawableRes int getDrawable(String tag) {
switch (tag) {
case QueueFragment.TAG:
return R.drawable.ic_playlist_play;
case InboxFragment.TAG:
return R.drawable.ic_inbox;
case AllEpisodesFragment.TAG:
return R.drawable.ic_feed;
case CompletedDownloadsFragment.TAG:
return R.drawable.ic_download;
case PlaybackHistoryFragment.TAG:
return R.drawable.ic_history;
case SubscriptionFragment.TAG:
return R.drawable.ic_folder;
case AddFeedFragment.TAG:
return R.drawable.ic_add;
default:
return 0;
}
}
public List<String> getFragmentTags() {
return Collections.unmodifiableList(fragmentTags);
}
@Override
public int getItemCount() {
int baseCount = getSubscriptionOffset();
if (showSubscriptionList) {
baseCount += itemAccess.getCount();
}
return baseCount;
}
@Override
public long getItemId(int position) {
int viewType = getItemViewType(position);
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 0;
}
}
@Override
public int getItemViewType(int position) {
if (0 <= position && position < fragmentTags.size()) {
return VIEW_TYPE_NAV;
} else if (position < getSubscriptionOffset()) {
return VIEW_TYPE_SECTION_DIVIDER;
} else {
return VIEW_TYPE_SUBSCRIPTION;
}
}
public int getSubscriptionOffset() {
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 void onBindViewHolder(@NonNull Holder holder, int position) {
int viewType = getItemViewType(position);
holder.itemView.setOnCreateContextMenuListener(null);
if (viewType == VIEW_TYPE_NAV) {
bindNavView(getLabel(fragmentTags.get(position)), position, (NavHolder) holder);
} else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
bindSectionDivider((DividerHolder) holder);
} else {
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);
} else {
bindTagView((NavDrawerData.TagDrawerItem) item, (FeedHolder) holder);
}
holder.itemView.setOnCreateContextMenuListener(itemAccess);
}
if (viewType != VIEW_TYPE_SECTION_DIVIDER) {
TypedValue typedValue = new TypedValue();
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));
holder.itemView.setOnTouchListener((v, e) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
itemAccess.onItemLongClick(position);
return false;
}
}
return false;
});
}
}
private void bindNavView(String title, int position, NavHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
}
holder.title.setText(title);
// reset for re-use
holder.count.setVisibility(View.GONE);
holder.count.setOnClickListener(null);
holder.count.setClickable(false);
String tag = fragmentTags.get(position);
if (tag.equals(QueueFragment.TAG)) {
int queueSize = itemAccess.getQueueSize();
if (queueSize > 0) {
holder.count.setText(NumberFormat.getInstance().format(queueSize));
holder.count.setVisibility(View.VISIBLE);
}
} else if (tag.equals(InboxFragment.TAG)) {
int unreadItems = itemAccess.getNumberOfNewItems();
if (unreadItems > 0) {
holder.count.setText(NumberFormat.getInstance().format(unreadItems));
holder.count.setVisibility(View.VISIBLE);
}
} else if (tag.equals(SubscriptionFragment.TAG)) {
int sum = itemAccess.getFeedCounterSum();
if (sum > 0) {
holder.count.setText(NumberFormat.getInstance().format(sum));
holder.count.setVisibility(View.VISIBLE);
}
} else if (tag.equals(CompletedDownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) {
int epCacheSize = UserPreferences.getEpisodeCacheSize();
// don't count episodes that can be reclaimed
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)
.setTitle(R.string.episode_cache_full_title)
.setMessage(R.string.episode_cache_full_message)
.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.setImageResource(getDrawable(fragmentTags.get(position)));
}
private void bindSectionDivider(DividerHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
}
if (UserPreferences.getSubscriptionsFilter().isEnabled() && showSubscriptionList) {
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 {
holder.itemView.setEnabled(false);
holder.feedsFilteredMsg.setVisibility(View.GONE);
}
}
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 void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) {
Feed feed = drawerItem.feed;
Activity context = activity.get();
if (context == null) {
return;
}
Glide.with(context)
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(holder.image);
if (feed.hasLastUpdateFailed()) {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams();
p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure);
holder.failure.setVisibility(View.VISIBLE);
} else {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams();
p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount);
holder.failure.setVisibility(View.GONE);
}
}
private void bindTagView(NavDrawerData.TagDrawerItem tag, FeedHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
}
if (tag.isOpen) {
holder.count.setVisibility(View.GONE);
}
Glide.with(context).clear(holder.image);
holder.image.setImageResource(R.drawable.ic_tag);
holder.failure.setVisibility(View.GONE);
}
static class Holder extends RecyclerView.ViewHolder {
public Holder(@NonNull View itemView) {
super(itemView);
}
}
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 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);
}
}
static class FeedHolder extends Holder {
final ImageView image;
final TextView title;
final ImageView 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();
NavDrawerData.DrawerItem getItem(int position);
boolean isSelected(int position);
int getQueueSize();
int getNumberOfNewItems();
int getNumberOfDownloadedItems();
int getReclaimableItems();
int getFeedCounterSum();
void onItemClick(int position);
boolean onItemLongClick(int position);
@Override
void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo);
}
}

View File

@ -1,91 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.annotation.SuppressLint;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
/**
* List adapter for the queue.
*/
public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
private static final String TAG = "QueueRecyclerAdapter";
private final SwipeActions swipeActions;
private boolean dragDropEnabled;
public QueueRecyclerAdapter(MainActivity mainActivity, SwipeActions swipeActions) {
super(mainActivity);
this.swipeActions = swipeActions;
dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked());
}
public void updateDragDropEnabled() {
dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked());
notifyDataSetChanged();
}
@Override
@SuppressLint("ClickableViewAccessibility")
protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) {
if (!dragDropEnabled || inActionMode()) {
holder.dragHandle.setVisibility(View.GONE);
holder.dragHandle.setOnTouchListener(null);
holder.coverHolder.setOnTouchListener(null);
} else {
holder.dragHandle.setVisibility(View.VISIBLE);
holder.dragHandle.setOnTouchListener((v1, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Log.d(TAG, "startDrag()");
swipeActions.startDrag(holder);
}
return false;
});
holder.coverHolder.setOnTouchListener((v1, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
boolean isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
float factor = isLtr ? 1 : -1;
if (factor * event.getX() < factor * 0.5 * v1.getWidth()) {
Log.d(TAG, "startDrag()");
swipeActions.startDrag(holder);
} else {
Log.d(TAG, "Ignoring drag in right half of the image");
}
}
return false;
});
}
holder.isInQueue.setVisibility(View.GONE);
}
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.queue_context, menu);
super.onCreateContextMenu(menu, v, menuInfo);
if (!inActionMode()) {
menu.findItem(R.id.multi_select).setVisible(true);
final boolean keepSorted = UserPreferences.isQueueKeepSorted();
if (getItem(0).getId() == getLongPressedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_top_item).setVisible(false);
}
if (getItem(getItemCount() - 1).getId() == getLongPressedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_bottom_item).setVisible(false);
}
} else {
menu.findItem(R.id.move_to_top_item).setVisible(false);
menu.findItem(R.id.move_to_bottom_item).setVisible(false);
}
}
}

View File

@ -1,200 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import java.util.HashSet;
/**
* Used by Recyclerviews that need to provide ability to select items.
*/
public abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
public static final int COUNT_AUTOMATICALLY = -1;
private ActionMode actionMode;
private final HashSet<Long> selectedIds = new HashSet<>();
private final Activity activity;
private OnSelectModeListener onSelectModeListener;
boolean shouldSelectLazyLoadedItems = false;
private int totalNumberOfItems = COUNT_AUTOMATICALLY;
public SelectableAdapter(Activity activity) {
this.activity = activity;
}
public void startSelectMode(int pos) {
if (inActionMode()) {
endSelectMode();
}
if (onSelectModeListener != null) {
onSelectModeListener.onStartSelectMode();
}
shouldSelectLazyLoadedItems = false;
selectedIds.clear();
selectedIds.add(getItemId(pos));
notifyDataSetChanged();
actionMode = activity.startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.multi_select_options, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
updateTitle();
toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false);
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.select_toggle) {
boolean selectAll = selectedIds.size() != getItemCount();
shouldSelectLazyLoadedItems = selectAll;
setSelected(0, getItemCount(), selectAll);
toggleSelectAllIcon(item, selectAll);
updateTitle();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
callOnEndSelectMode();
actionMode = null;
shouldSelectLazyLoadedItems = false;
selectedIds.clear();
notifyDataSetChanged();
}
});
updateTitle();
}
/**
* End action mode if currently in select mode, otherwise do nothing
*/
public void endSelectMode() {
if (inActionMode()) {
callOnEndSelectMode();
actionMode.finish();
}
}
public boolean isSelected(int pos) {
return selectedIds.contains(getItemId(pos));
}
/**
* Set the selected state of item at given position
*
* @param pos the position to select
* @param selected true for selected state and false for unselected
*/
public void setSelected(int pos, boolean selected) {
if (selected) {
selectedIds.add(getItemId(pos));
} else {
selectedIds.remove(getItemId(pos));
}
updateTitle();
}
/**
* Set the selected state of item for a given range
*
* @param startPos start position of range, inclusive
* @param endPos end position of range, inclusive
* @param selected indicates the selection state
* @throws IllegalArgumentException if start and end positions are not valid
*/
public void setSelected(int startPos, int endPos, boolean selected) throws IllegalArgumentException {
for (int i = startPos; i < endPos && i < getItemCount(); i++) {
setSelected(i, selected);
}
notifyItemRangeChanged(startPos, (endPos - startPos));
}
protected void toggleSelection(int pos) {
setSelected(pos, !isSelected(pos));
notifyItemChanged(pos);
if (selectedIds.size() == 0) {
endSelectMode();
}
}
public boolean inActionMode() {
return actionMode != null;
}
public int getSelectedCount() {
return selectedIds.size();
}
private void toggleSelectAllIcon(MenuItem selectAllItem, boolean allSelected) {
if (allSelected) {
selectAllItem.setIcon(R.drawable.ic_select_none);
selectAllItem.setTitle(R.string.deselect_all_label);
} else {
selectAllItem.setIcon(R.drawable.ic_select_all);
selectAllItem.setTitle(R.string.select_all_label);
}
}
void updateTitle() {
if (actionMode == null) {
return;
}
int totalCount = getItemCount();
int selectedCount = selectedIds.size();
if (totalNumberOfItems != COUNT_AUTOMATICALLY) {
totalCount = totalNumberOfItems;
if (shouldSelectLazyLoadedItems) {
selectedCount += (totalNumberOfItems - getItemCount());
}
}
actionMode.setTitle(activity.getResources()
.getQuantityString(R.plurals.num_selected_label, selectedIds.size(),
selectedCount, totalCount));
}
public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) {
this.onSelectModeListener = onSelectModeListener;
}
private void callOnEndSelectMode() {
if (onSelectModeListener != null) {
onSelectModeListener.onEndSelectMode();
}
}
public boolean shouldSelectLazyLoadedItems() {
return shouldSelectLazyLoadedItems;
}
/**
* Sets the total number of items that could be lazy-loaded.
* Can also be set to {@link #COUNT_AUTOMATICALLY} to simply use {@link #getItemCount}
*/
public void setTotalNumberOfItems(int totalNumberOfItems) {
this.totalNumberOfItems = totalNumberOfItems;
}
public interface OnSelectModeListener {
void onStartSelectMode();
void onEndSelectMode();
}
}

View File

@ -1,59 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import java.util.List;
/**
* Displays a list of items that have a subtitle and an icon.
*/
public class SimpleIconListAdapter<T extends SimpleIconListAdapter.ListItem> extends ArrayAdapter<T> {
private final Context context;
private final List<T> listItems;
public SimpleIconListAdapter(Context context, List<T> listItems) {
super(context, R.layout.simple_icon_list_item, listItems);
this.context = context;
this.listItems = listItems;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
view = View.inflate(context, R.layout.simple_icon_list_item, null);
}
ListItem item = listItems.get(position);
((TextView) view.findViewById(R.id.title)).setText(item.title);
((TextView) view.findViewById(R.id.subtitle)).setText(item.subtitle);
Glide.with(context)
.load(item.imageUrl)
.apply(new RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
.dontAnimate())
.into(((ImageView) view.findViewById(R.id.icon)));
return view;
}
public static class ListItem {
public final String title;
public final String subtitle;
public final String imageUrl;
public ListItem(String title, String subtitle, String imageUrl) {
this.title = title;
this.subtitle = subtitle;
this.imageUrl = imageUrl;
}
}
}

View File

@ -1,296 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.ui.common.TriangleLabelView;
/**
* Adapter for subscriptions
*/
public class SubscriptionsRecyclerAdapter extends SelectableAdapter<SubscriptionsRecyclerAdapter.SubscriptionViewHolder>
implements View.OnCreateContextMenuListener {
private static final int COVER_WITH_TITLE = 1;
private final WeakReference<MainActivity> mainActivityRef;
private List<NavDrawerData.DrawerItem> listItems;
private NavDrawerData.DrawerItem selectedItem = null;
int longPressedPosition = 0; // used to init actionMode
public SubscriptionsRecyclerAdapter(MainActivity mainActivity) {
super(mainActivity);
this.mainActivityRef = new WeakReference<>(mainActivity);
this.listItems = new ArrayList<>();
setHasStableIds(true);
}
public Object getItem(int position) {
return listItems.get(position);
}
public NavDrawerData.DrawerItem getSelectedItem() {
return selectedItem;
}
@NonNull
@Override
public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false);
TextView feedTitle = itemView.findViewById(R.id.txtvTitle);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) feedTitle.getLayoutParams();
int topAndBottomItemId = R.id.imgvCover;
int belowItemId = 0;
if (viewType == COVER_WITH_TITLE) {
topAndBottomItemId = 0;
belowItemId = R.id.imgvCover;
feedTitle.setBackgroundColor(
ContextCompat.getColor(feedTitle.getContext(), R.color.feed_text_bg));
int padding = (int) convertDpToPixel(feedTitle.getContext(), 6);
feedTitle.setPadding(padding, padding, padding, padding);
}
params.addRule(RelativeLayout.BELOW, belowItemId);
params.addRule(RelativeLayout.ALIGN_TOP, topAndBottomItemId);
params.addRule(RelativeLayout.ALIGN_BOTTOM, topAndBottomItemId);
feedTitle.setLayoutParams(params);
feedTitle.setSingleLine(viewType == COVER_WITH_TITLE);
return new SubscriptionViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull SubscriptionViewHolder holder, int position) {
NavDrawerData.DrawerItem drawerItem = listItems.get(position);
boolean isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED;
holder.bind(drawerItem);
holder.itemView.setOnCreateContextMenuListener(this);
if (inActionMode()) {
if (isFeed) {
holder.selectCheckbox.setVisibility(View.VISIBLE);
holder.selectView.setVisibility(View.VISIBLE);
}
holder.selectCheckbox.setChecked((isSelected(position)));
holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked)
-> setSelected(holder.getBindingAdapterPosition(), isChecked));
holder.imageView.setAlpha(0.6f);
holder.count.setVisibility(View.GONE);
} else {
holder.selectView.setVisibility(View.GONE);
holder.imageView.setAlpha(1.0f);
}
holder.itemView.setOnLongClickListener(v -> {
if (!inActionMode()) {
if (isFeed) {
longPressedPosition = holder.getBindingAdapterPosition();
}
selectedItem = drawerItem;
}
return false;
});
holder.itemView.setOnTouchListener((v, e) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
if (!inActionMode()) {
if (isFeed) {
longPressedPosition = holder.getBindingAdapterPosition();
}
selectedItem = drawerItem;
}
}
}
return false;
});
holder.itemView.setOnClickListener(v -> {
if (isFeed) {
if (inActionMode()) {
holder.selectCheckbox.setChecked(!isSelected(holder.getBindingAdapterPosition()));
} else {
Fragment fragment = FeedItemlistFragment
.newInstance(((NavDrawerData.FeedDrawerItem) drawerItem).feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
}
} else if (!inActionMode()) {
Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle());
mainActivityRef.get().loadChildFragment(fragment);
}
});
}
@Override
public int getItemCount() {
return listItems.size();
}
@Override
public long getItemId(int position) {
return listItems.get(position).id;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (inActionMode() || selectedItem == null) {
return;
}
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
if (selectedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
inflater.inflate(R.menu.nav_feed_context, menu);
menu.findItem(R.id.multi_select).setVisible(true);
} else {
inflater.inflate(R.menu.nav_folder_context, menu);
}
menu.setHeaderTitle(selectedItem.getTitle());
}
public boolean onContextItemSelected(MenuItem item) {
if (item.getItemId() == R.id.multi_select) {
startSelectMode(longPressedPosition);
return true;
}
return false;
}
public List<Feed> getSelectedItems() {
List<Feed> items = new ArrayList<>();
for (int i = 0; i < getItemCount(); i++) {
if (isSelected(i)) {
NavDrawerData.DrawerItem drawerItem = listItems.get(i);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
items.add(feed);
}
}
}
return items;
}
public void setItems(List<NavDrawerData.DrawerItem> listItems) {
this.listItems = listItems;
}
@Override
public void setSelected(int pos, boolean selected) {
NavDrawerData.DrawerItem drawerItem = listItems.get(pos);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
super.setSelected(pos, selected);
}
}
@Override
public int getItemViewType(int position) {
return UserPreferences.shouldShowSubscriptionTitle() ? COVER_WITH_TITLE : 0;
}
public class SubscriptionViewHolder extends RecyclerView.ViewHolder {
private final TextView feedTitle;
private final ImageView imageView;
private final TriangleLabelView count;
private final FrameLayout selectView;
private final CheckBox selectCheckbox;
public SubscriptionViewHolder(@NonNull View itemView) {
super(itemView);
feedTitle = itemView.findViewById(R.id.txtvTitle);
imageView = itemView.findViewById(R.id.imgvCover);
count = itemView.findViewById(R.id.triangleCountView);
selectView = itemView.findViewById(R.id.selectView);
selectCheckbox = itemView.findViewById(R.id.selectCheckBox);
}
public void bind(NavDrawerData.DrawerItem drawerItem) {
Drawable drawable = AppCompatResources.getDrawable(selectView.getContext(),
R.drawable.ic_checkbox_background);
selectView.setBackground(drawable); // Setting this in XML crashes API <= 21
feedTitle.setText(drawerItem.getTitle());
imageView.setContentDescription(drawerItem.getTitle());
feedTitle.setVisibility(View.VISIBLE);
if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL) {
count.setCorner(TriangleLabelView.Corner.TOP_LEFT);
}
if (drawerItem.getCounter() > 0) {
count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter()));
count.setVisibility(View.VISIBLE);
} else {
count.setVisibility(View.GONE);
}
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombind = feed.isLocalFeed()
&& feed.getImageUrl() != null && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER);
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(feedTitle, textAndImageCombind)
.withCoverView(imageView)
.load();
} else {
new CoverLoader(mainActivityRef.get())
.withResource(R.drawable.ic_tag)
.withPlaceholderView(feedTitle, true)
.withCoverView(imageView)
.load();
}
}
}
public static float convertDpToPixel(Context context, float dp) {
return dp * context.getResources().getDisplayMetrics().density;
}
public static class GridDividerItemDecorator extends RecyclerView.ItemDecoration {
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void getItemOffsets(@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
Context context = parent.getContext();
int insetOffset = (int) convertDpToPixel(context, 1f);
outRect.set(insetOffset, insetOffset, insetOffset, insetOffset);
}
}
}

View File

@ -1,41 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.download.DownloadService;
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;
public class CancelDownloadActionButton extends ItemActionButton {
public CancelDownloadActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.cancel_download_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_cancel;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
DownloadService.cancel(context, media.getDownload_url());
if (UserPreferences.isEnableAutodownload()) {
item.disableAutoDownload();
DBWriter.setFeedItem(item);
}
}
}

View File

@ -1,43 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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 {
public DeleteActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.delete_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_delete;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
DBWriter.deleteFeedMediaOfItem(context, media.getId());
}
@Override
public int getVisibility() {
return (item.getMedia() != null && item.getMedia().isDownloaded()) ? View.VISIBLE : View.INVISIBLE;
}
}

View File

@ -1,66 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.service.download.DownloadService;
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.util.NetworkUtils;
public class DownloadActionButton extends ItemActionButton {
public DownloadActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.download_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_download;
}
@Override
public int getVisibility() {
return item.getFeed().isLocalFeed() ? View.INVISIBLE : View.VISIBLE;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null || shouldNotDownload(media)) {
return;
}
UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD);
if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) {
DownloadService.download(context, false, DownloadRequestCreator.create(item.getMedia()).build());
} else if (MobileDownloadHelper.userChoseAddToQueue() && !item.isTagged(FeedItem.TAG_QUEUE)) {
DBWriter.addQueueItem(context, item);
Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show();
} else {
MobileDownloadHelper.confirmMobileDownload(context, item);
}
}
private boolean shouldNotDownload(@NonNull FeedMedia media) {
boolean isDownloading = DownloadService.isDownloadingFile(media.getDownload_url());
return isDownloading || media.isDownloaded();
}
}

View File

@ -1,64 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import android.view.View;
import de.danoeh.antennapod.core.service.download.DownloadService;
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.util.FeedItemUtil;
public abstract class ItemActionButton {
FeedItem item;
ItemActionButton(FeedItem item) {
this.item = item;
}
@StringRes
public abstract int getLabel();
@DrawableRes
public abstract int getDrawable();
public abstract void onClick(Context context);
public int getVisibility() {
return View.VISIBLE;
}
@NonNull
public static ItemActionButton forItem(@NonNull FeedItem item) {
final FeedMedia media = item.getMedia();
if (media == null) {
return new MarkAsPlayedActionButton(item);
}
final boolean isDownloadingMedia = DownloadService.isDownloadingFile(media.getDownload_url());
if (FeedItemUtil.isCurrentlyPlaying(media)) {
return new PauseActionButton(item);
} else if (item.getFeed().isLocalFeed()) {
return new PlayLocalActionButton(item);
} else if (media.isDownloaded()) {
return new PlayActionButton(item);
} else if (isDownloadingMedia) {
return new CancelDownloadActionButton(item);
} else if (UserPreferences.isStreamOverDownload()) {
return new StreamActionButton(item);
} else {
return new DownloadActionButton(item);
}
}
public void configure(@NonNull View button, @NonNull ImageView icon, Context context) {
button.setVisibility(getVisibility());
button.setContentDescription(context.getString(getLabel()));
button.setOnClickListener((view) -> onClick(context));
icon.setImageResource(getDrawable());
}
}

View File

@ -1,41 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import android.view.View;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBWriter;
public class MarkAsPlayedActionButton extends ItemActionButton {
public MarkAsPlayedActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return (item.hasMedia() ? R.string.mark_read_label : R.string.mark_read_no_media_label);
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_check;
}
@Override
public void onClick(Context context) {
if (!item.isPlayed()) {
DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
}
}
@Override
public int getVisibility() {
return (item.isPlayed()) ? View.INVISIBLE : View.VISIBLE;
}
}

View File

@ -1,49 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.download.DownloadRequestCreator;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
class MobileDownloadHelper {
private static long addToQueueTimestamp;
private static long allowMobileDownloadTimestamp;
private static final int TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000;
static boolean userChoseAddToQueue() {
return System.currentTimeMillis() - addToQueueTimestamp < TEN_MINUTES_IN_MILLIS;
}
static boolean userAllowedMobileDownloads() {
return System.currentTimeMillis() - allowMobileDownloadTimestamp < TEN_MINUTES_IN_MILLIS;
}
static void confirmMobileDownload(final Context context, final FeedItem item) {
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(R.string.confirm_mobile_download_dialog_title)
.setMessage(R.string.confirm_mobile_download_dialog_message)
.setPositiveButton(context.getText(R.string.confirm_mobile_download_dialog_enable_temporarily),
(dialog, which) -> downloadFeedItems(context, item));
if (!DBReader.getQueueIDList().contains(item.getId())) {
builder.setMessage(R.string.confirm_mobile_download_dialog_message_not_in_queue)
.setNeutralButton(R.string.confirm_mobile_download_dialog_only_add_to_queue,
(dialog, which) -> addToQueue(context, item));
}
builder.show();
}
private static void addToQueue(Context context, FeedItem item) {
addToQueueTimestamp = System.currentTimeMillis();
DBWriter.addQueueItem(context, item);
}
private static void downloadFeedItems(Context context, FeedItem item) {
allowMobileDownloadTimestamp = System.currentTimeMillis();
DownloadService.download(context, true, DownloadRequestCreator.create(item.getMedia()).build());
}
}

View File

@ -1,43 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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;
public class PauseActionButton extends ItemActionButton {
public PauseActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.pause_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_pause;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
if (media == null) {
return;
}
if (FeedItemUtil.isCurrentlyPlaying(media)) {
IntentUtils.sendLocalBroadcast(context, ACTION_PAUSE_PLAY_CURRENT_EPISODE);
}
}
}

View File

@ -1,50 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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;
public class PlayActionButton extends ItemActionButton {
public PlayActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.play_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_play_24dp;
}
@Override
public void onClick(Context context) {
FeedMedia media = item.getMedia();
if (media == null) {
return;
}
if (!media.fileExists()) {
DBTasks.notifyMissingFeedMediaFile(context, media);
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -1,46 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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;
public class PlayLocalActionButton extends ItemActionButton {
public PlayLocalActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.play_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_play_24dp;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -1,56 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
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;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.dialog.StreamingConfirmationDialog;
public class StreamActionButton extends ItemActionButton {
public StreamActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.stream_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_stream;
}
@Override
public void onClick(Context context) {
final FeedMedia media = item.getMedia();
if (media == null) {
return;
}
UsageStatistics.logAction(UsageStatistics.ACTION_STREAM);
if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(context, media).show();
return;
}
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.start();
if (media.getMediaType() == MediaType.VIDEO) {
context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
}
}
}

View File

@ -1,38 +0,0 @@
package de.danoeh.antennapod.adapter.actionbutton;
import android.content.Context;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.util.IntentUtils;
public class VisitWebsiteActionButton extends ItemActionButton {
public VisitWebsiteActionButton(FeedItem item) {
super(item);
}
@Override
@StringRes
public int getLabel() {
return R.string.visit_website_label;
}
@Override
@DrawableRes
public int getDrawable() {
return R.drawable.ic_web;
}
@Override
public void onClick(Context context) {
IntentUtils.openInBrowser(context, item.getLink());
}
@Override
public int getVisibility() {
return (item.getLink() == null) ? View.INVISIBLE : View.VISIBLE;
}
}

Some files were not shown because too many files have changed in this diff Show More