Merge branch 'develop' into folders

This commit is contained in:
ByteHamster 2021-03-05 10:09:10 +01:00
commit f76d3ad09e
311 changed files with 8354 additions and 6892 deletions

View File

@ -45,7 +45,7 @@ workflows:
destination: app-play-debug.apk
- run:
name: Execute debug unit tests
command: ./gradlew :core:testPlayDebugUnitTest -PdisablePreDex
command: ./gradlew testPlayDebugUnitTest -PdisablePreDex
- build:
name: Build release
build-steps:
@ -54,19 +54,19 @@ workflows:
command: ./gradlew assembleRelease -PdisablePreDex
- run:
name: Execute release unit tests
command: ./gradlew :core:testPlayReleaseUnitTest -PdisablePreDex
command: ./gradlew testPlayReleaseUnitTest -PdisablePreDex
- build:
name: Build integration tests
build-steps:
- run:
name: Build integration tests
command: ./gradlew :app:assemblePlayDebugAndroidTest -PdisablePreDex
command: ./gradlew assemblePlayDebugAndroidTest -PdisablePreDex
- build:
name: Build free
build-steps:
- run:
name: Build free (for F-Droid)
command: ./gradlew assembleFreeRelease -PdisablePreDex -PfreeBuild
command: ./gradlew assembleFreeRelease -PdisablePreDex
static-analysis:
jobs:
@ -85,19 +85,8 @@ workflows:
curl -s -L https://github.com/yangziwen/diff-checkstyle/releases/download/0.0.4/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 $branchBaseCommit
- build:
name: Lint app
name: Lint
build-steps:
- run:
name: Lint app
command: ./gradlew app:lintPlayRelease
- run:
name: Lint core
command: ./gradlew core:lintPlayRelease
- store_artifacts:
name: Uploading app lint reports
path: app/build/reports/lint-results-playRelease.html
destination: lint-results-app.html
- store_artifacts:
name: Uploading core lint reports
path: core/build/reports/lint-results-playRelease.html
destination: lint-results-core.html
name: Lint
command: ./gradlew lintPlayRelease

View File

@ -4,6 +4,7 @@ host = https://www.transifex.com
[antennapod.core-values]
source_file = core/src/main/res/values/strings.xml
source_lang = en
trans.ar = core/src/main/res/values-ar/strings.xml
trans.br = core/src/main/res/values-br/strings.xml
trans.ca = core/src/main/res/values-ca/strings.xml
trans.cs_CZ = core/src/main/res/values-cs/strings.xml
@ -29,6 +30,7 @@ trans.pl_PL = core/src/main/res/values-pl/strings.xml
trans.pt = core/src/main/res/values-pt/strings.xml
trans.pt_BR = core/src/main/res/values-pt-rBR/strings.xml
trans.ru_RU = core/src/main/res/values-ru/strings.xml
trans.sk = core/src/main/res/values-sk/strings.xml
trans.sv_SE = core/src/main/res/values-sv/strings.xml
trans.tr = core/src/main/res/values-tr/strings.xml
trans.uk_UA = core/src/main/res/values-uk/strings.xml
@ -58,7 +60,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/full-description.txt
trans.hu = app/src/main/play/listings/hu-HU/full-description.txt
trans.id = app/src/main/play/listings/id/full-description.txt
trans.it_IT = app/src/main/play/listings/it-IT/full-description.txt
trans.iw = app/src/main/play/listings/iw-IL/full-description.txt
trans.ja = app/src/main/play/listings/ja-JP/full-description.txt
trans.ko = app/src/main/play/listings/ko-KR/full-description.txt
trans.lt = app/src/main/play/listings/lt/full-description.txt
@ -68,6 +69,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/full-description.txt
trans.pt = app/src/main/play/listings/pt-PT/full-description.txt
trans.ro_RO = app/src/main/play/listings/ro/full-description.txt
trans.ru_RU = app/src/main/play/listings/ru-RU/full-description.txt
trans.sk = app/src/main/play/listings/sk/full-description.txt
trans.sl_SI = app/src/main/play/listings/sl/full-description.txt
trans.sv_SE = app/src/main/play/listings/sv-SE/full-description.txt
trans.tr = app/src/main/play/listings/tr-TR/full-description.txt
@ -98,7 +100,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/short-description.txt
trans.hu = app/src/main/play/listings/hu-HU/short-description.txt
trans.id = app/src/main/play/listings/id/short-description.txt
trans.it_IT = app/src/main/play/listings/it-IT/short-description.txt
trans.iw = app/src/main/play/listings/iw-IL/short-description.txt
trans.ja = app/src/main/play/listings/ja-JP/short-description.txt
trans.ko = app/src/main/play/listings/ko-KR/short-description.txt
trans.lt = app/src/main/play/listings/lt/short-description.txt
@ -108,6 +109,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/short-description.txt
trans.pt = app/src/main/play/listings/pt-PT/short-description.txt
trans.ro_RO = app/src/main/play/listings/ro/short-description.txt
trans.ru_RU = app/src/main/play/listings/ru-RU/short-description.txt
trans.sk = app/src/main/play/listings/sk/short-description.txt
trans.sl_SI = app/src/main/play/listings/sl/short-description.txt
trans.sv_SE = app/src/main/play/listings/sv-SE/short-description.txt
trans.tr = app/src/main/play/listings/tr-TR/short-description.txt

View File

@ -1,471 +0,0 @@
Change Log
==========
Version 1.8.1
-------------
* Enabled picture-in-picture for video podcasts by default (by @ByteHamster)
* Fixed podcast discovery not showing local trends (by @tonytamsf)
* Various bug fixes and improvements (by @ByteHamster)
Version 1.8.0
-------------
* Added per-feed playback speed setting (by @spacecowboy)
* Support sorting in Podcast screen (by @orionlee)
* Option to show stream button rather than download in lists (by @dsmith47)
* Option to replace Episode cover with Podcast cover (by @xgouchet)
* Transparent widget (by @M-arcel)
* User interface tweaks (by @ByteHamster)
* Tons of bug fixes and improvements
Version 1.7.3
-------------
* Display episode image on widget (by @brad)
* Added checkbox to keep queue sorted (by @damoasda)
* New UI for "Add podcast" screen (by @ByteHamster)
* Added batch editing to the queue (by @ByteHamster)
* Added option to adapt remaining time to playback speed (by @CedricCabessa)
* Removed broken Flattr integration (by @ByteHamster)
* Added filter to "All episodes" list (by @jhunnius)
* Tons of bug fixes and performance improvements
Version 1.7.2
-------------
* Added configurable behavior of the back button
* Added delete option to episode's context menu
* New UI for batch edit feature
* Set number of columns in subscription list
* Lots of bug fixes
Version 1.7.1
-------------
* Fix for database corruption
Version 1.7.0
-------------
* NEW ExoPlayer (experimental)
* Fix for Bluetooth Forward (Oreo)
* Preference redesign + search
* Notification improvements
* Different screens for feed info and settings
* Sort Queue with Random or Smart Shuffle
* True Black Theme for AMOLED
* Improvements to feed parsing
* Fix for app being killed by Android Oreo
Version 1.6.5
-------------
* Fix database corruption
* Improvements to Feed parsing
Version 1.6.4
-------------
* Fixes issues on Android Oreo
* Avoids duplicate chapters
* Experimental: Database import & export
Version 1.6.3
-------------
* New features:
* Support for Android Auto
* Sort feeds by number of played episodes
* Statistics modes
* Setting: Enqueue downloaded
* Launch screen
* Improvements
* Chapter duration
* Feed title in deletion confirmation
* Fixes:
* Episodes refresh spinner
* Publication date parsing
* Unknown mime type
Version 1.6.2
-------------
* New features:
* Integration of fyyd Podcast Search Engine
* Export subscriptions as HTML
* Rename feeds
* Auto-enable sleep timer
* "has media" filter
* Force gpodder full sync
* Improvements:
* Better support for Atom feeds, e.g. summary tag
* Confirmation dialog on mark all as seen
* Number of downloaded episodes in subscription counter
* Gpodder sync error optional
* Search results
* MRSS support
* Sanitize HTML from Atom feed
* Fixes:
* Reset sleep timer on shake to current waiting time
* Cast dialog image
* Mini player not showing up
* Audio player cover fragment
* Prevent out of memory and casting crashes
Version 1.6.0
-------------
* New features:
* Experimental Chromecast support
* Subscription overview
* Proxy support
* Statistics
* Manual gpodder.net sync
* Fixes:
* Audioplayer controls
* Audio ducking
* Video control fade-out
* External media controls
* Feed parsing
Version 1.5.0
-------------
* Exclude episodes from auto download by keyword
* Configure feeds to prevent them from refreshing automatically
* Improved audio player
* Improved UI
* Bug fixes
Version 1.4.1
-------------
* Performance improvements
* Hardware buttons now ff and rewind instead of skipping
* Option to have forward button skip
* Option to send crash reports directly to developers
* Highlight currently playing episode
* Widget improvements
Version 1.4.0.12
----------------
* Fix for crash on Huawei devices (media buttons may not work)
Version 1.4
-----------
* BLUETOOTH PERMISSION: Needed to be able to resume playback when a Bluetooth device reconnects with your phone
* VIBRATE PERMISSION: Used optionally with the sleep timer
* Native variable speed playback (experimental via options)
* Improved sleep timer
* Mark episodes as 'favorite'
* Notification can skip episodes
* Keep episodes when skipping them
* Episode art on lock screen
* Flexible episode cleanup
* Rewind after pause
* Usability improvements
* Bug fixes
Version 1.3
-----------
* Bulk actions on feed episodes (download, queue, delete)
* Reduced space used by images
* Automatic refresh at a certain time of day
* Customizable indicators and sorting for feeds
* Ability to share feeds
* Improved auto download
* Many fixes and usability improvements
Version 1.2
-----------
* Optionally disable swiping and dragging in the queue
* Resume playback after phone call
* Filter episodes in the Podcast feed
* Hide items in the Nav drawer
* Customize times for fast forward and rewind
* Resolved issues with opening some OPML files
* Various bug fixes and usability improvements
Version 1.1
-----------
* iTunes podcast integration
* Swipe to remove items from the queue
* Set the number of parallel downloads
* Fix for gpodder.net on old devices
* Fixed date problems for some feeds
* Display improvements
* Usability improvements
* Several other bugfixes
Version 1.0
-----------
* The queue can now be sorted
* Added option to delete episode after playback
* Fixed a bug that caused chapters to be displayed multiple times
* Several other improvements and bugfixes
Version 0.9.9.6
---------------
* Fixed problems related to variable playback speed plugins
* Fixed automatic feed update problems
* Several other bugfixes and improvements
Version 0.9.9.5
---------------
* Added support for paged feeds
* Improved user interface
* Added Japanese and Turkish translations
* Fixed more image loading problems
* Other bugfixes and improvements
Version 0.9.9.4
---------------
* Added option to keep notification and lockscreen controls when playback is paused
* Fixed a bug where episode images were not loaded correctly
* Fixed battery usage problems
Version 0.9.9.3
---------------
* Fixed video playback problems
* Improved image loading
* Other bugfixes and improvements
Version 0.9.9.2
---------------
* Added support for feed discovery if a website URL is entered
* Added support for 'next'/'previous' media keys
* Improved sleep timer
* Timestamps in shownotes can now be used to jump to a specific position
* Automatic Flattring is now configurable
* Several bugfixes and improvements
Version 0.9.9.1
---------------
* Several bugfixes and improvements
Version 0.9.9.0
---------------
* New user interface
* Failed downloads are now resumed when restarted
* Added support for Podlove Alternate Feeds
* Added support for "pcast"-protocol
* Added backup & restore functionality. This feature has to be enabled in the Android settings in order to work
* Several bugfixes and improvements
Version 0.9.8.3
---------------
* Added support for password-protected feeds and episodes
* Added support for more types of episode images
* Added Hebrew translation
* Several bugfixes and improvements
Version 0.9.8.2
---------------
* Several bugfixes and improvements
* Added Korean translation
Version 0.9.8.1
---------------
* Added option to flattr an episode automatically after 80 percent of the episode have been played
* Added Polish translation
* Several bugfixes and improvements
Version 0.9.8.0
---------------
* Added access to the gpodder.net directory
* Added ability to sync podcast subscriptions with the gpodder.net service
* Automatic download can now be turned on or off for specific podcasts
* Added option to pause playback when another app is playing sounds
* Added Dutch and Hindi translation
* Resolved a problem with automatic podcast updates
* Resolved a problem with the buttons' visibility in the episode screen
* Resolved a problem where episodes would be re-downloaded unnecessarily
* Several other bugfixes and usability improvements
Version 0.9.7.5
---------------
* Reduced application startup time
* Reduced memory usage
* Added option to change the playback speed
* Added Swedish translation
* Several bugfixes and improvements
Version 0.9.7.4
---------------
* Episode cache size can now be set to unlimited
* Removing an episode in the queue via sliding can now be undone
* Added support for Links in MP3 chapters
* Added Czech(Czech Republic), Azerbaijani and Portuguese translations
* Several bugfixes and improvements
Version 0.9.7.3
---------------
* Bluetooth devices now display metadata during playback (requires AVRCP 1.3 or higher)
* User interface improvements
* Several bugfixes
Version 0.9.7.2
---------------
* Automatic download can now be disabled
* Added Italian (Italy) translation
* Several bugfixes
Version 0.9.7.1
---------------
* Added automatic download of new episodes
* Added option to specify the number of downloaded episodes to keep on the device
* Added support for playback of external media files
* Several improvements and bugfixes
* Added Catalan translation
Version 0.9.7
-------------
* Improved user interface
* OPML files can now be imported by selecting them in a file browser
* The queue can now be organized via drag & drop
* Added expandable notifications (only supported on Android 4.1 and above)
* Added Danish, French, Romanian (Romania) and Ukrainian (Ukraine) translation (thanks to all translators!)
* Several bugfixes and minor improvements
Version 0.9.6.4
---------------
* Added Chinese translation (Thanks tupunco!)
* Added Portuguese (Brazil) translation (Thanks mbaltar!)
* Several bugfixes
Version 0.9.6.3
---------------
* Added the ability change the location of AntennaPod's data folder
* Added Spanish translation (Thanks frandavid100!)
* Solved problems with several feeds
Version 0.9.6.2
---------------
* Fixed import problems with some OPML files
* Fixed download problems
* AntennaPod now recognizes changes of episode information
* Other improvements and bugfixes
Version 0.9.6.1
---------------
* Added dark theme
* Several bugfixes and improvements
Version 0.9.6
-------------
* Added support for VorbisComment chapters
* AntennaPod now shows items as 'in progress' when playback has started
* Reduced memory usage
* Added support for more feed types
* Several bugfixes
Version 0.9.5.3
---------------
* Fixed crash when trying to start playback on some devices
* Fixed problems with some feeds
* Other bugfixes and improvements
Version 0.9.5.2
---------------
* Media player now doesn't use network bandwidth anymore if not in use
* Other improvements and bugfixes
Version 0.9.5.1
---------------
* Added playback history
* Improved behavior of download report notifications
* Improved support for headset controls
* Bugfixes in the feed parser
* Moved 'OPML import' button into the 'add feed' screen and the 'OPML export' button into the settings screen
Version 0.9.5
-------------
* Experimental support for MP3 chapters
* New menu options for the 'new' list and the queue
* Auto-delete feature
* Better Download error reports
* Several Bugfixes
Version 0.9.4.6
---------------
* Enabled support for small-screen devices
* Disabling the sleep timer should now work again
Version 0.9.4.5
---------------
* Added Russian translation (Thanks older!)
* Added German translation
* Several bugfixes
Version 0.9.4.4
---------------
* Added player controls at the bottom of the main screen and the feedlist screens
* Improved media playback
Version 0.9.4.3
---------------
* Fixed several bugs in the feed parser
* Improved behavior of download reports
Version 0.9.4.2
---------------
* Fixed bug in the OPML importer
* Reduced memory usage of images
* Fixed download problems on some devices
Version 0.9.4.1
---------------
* Changed behavior of download notifications
Version 0.9.4
-------------
* Faster and more reliable downloads
* Added lockscreen player controls for Android 4.x devices
* Several bugfixes
Version 0.9.3.1
---------------
* Added preference to hide feed items which don't have an episode
* Improved image size for some some screen sizes
* Added grid view for large screens
* Several bugfixes
Version 0.9.3
-------------
* MiroGuide integration
* Bugfixes in the audio- and videoplayer
* Automatically add feeds to the queue when they have been downloaded
Version 0.9.2
-------------
* Bugfixes in the user interface
* GUID and ID attributes are now recognized by the Feedparser
* Stability improvements when adding several feeds at the same time
* Fixed bugs that occured when adding certain feeds
Version 0.9.1.1
--------------------
* Changed Flattr credentials
* Improved layout of Feed information screen
* AntennaPod is now open source! The source code is available at https://github.com/danieloeh/AntennaPod
Version 0.9.1
-----------------
* Added support for links in SimpleChapters
* Bugfix: Current Chapter wasn't always displayed correctly
Version 0.9
--------------
* OPML export
* Flattr integration
* Sleep timer
Version 0.8.2
-------------
* Added search
* Improved OPML import experience
* More bugfixes
Version 0.8.1
------------
* Added support for SimpleChapters
* OPML import

View File

@ -35,3 +35,24 @@ Submit a pull request
- 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 [coniguration 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/).
Testing and Verifying
--------------------------
As a developer contributing to AntennaPod, we ask that you test the feature yourself manually and better yet, add unit and functional tests to any feature of bug you fix.
### Running Unit Tests
* `./gradlew :core:testPlayDebugUnitTest`
### Running Integration Tests
#### Using Android Studio
* Create a configuration via 'Run->Edit Configurations...'
<img width="768" alt="antennapod-run-tests"
src="https://user-images.githubusercontent.com/149837/105122859-e1317180-5a8b-11eb-8d45-d54a3b051a9b.png">
#### Using the command line
* Start an AVD or plug in your phone
* `sh .github/workflows/runTests.sh`

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ You can use the [AntennaPod Forum](https://forum.antennapod.org/) for discussion
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!).
## Help to test AntennaPod
AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Alpha and Beta testing programmes](https://github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod)!
AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Beta testing program](https://antennapod.org/documentation/general/beta)! If a bug is reported during the beta period, chances are high that it will be fixed before the stable version. If it is reported later, fixing might take another full beta cycle. So definitely let us know if something is not right.
## License

View File

@ -19,11 +19,10 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// Version code schema:
// "1.2.3-SNAPSHOT" -> 1020300
// "1.2.3-RC4" -> 1020304
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 2020000
versionName "2.2.0"
versionCode 2020001
versionName "2.2.0-beta1"
multiDexEnabled false
vectorDrawables.useSupportLibrary true
@ -156,14 +155,9 @@ android {
}
dependencies {
freeImplementation project(":core")
// free build hack: skip some dependencies
if (!doFreeBuild()) {
playImplementation project(":core")
implementation 'com.google.android.play:core:1.8.0'
} else {
System.out.println("app: free build hack, skipping some dependencies")
}
implementation project(":core")
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
@ -191,14 +185,15 @@ dependencies {
implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion"
implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion"
implementation 'com.yqritc:recyclerview-flexibledivider:1.4.0'
implementation 'com.github.shts:TriangleLabelView:1.1.2'
implementation 'com.leinardi.android:speed-dial:3.1.1'
implementation 'com.github.leinardi:FloatingActionButtonSpeedDial:3.1.1'
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
implementation 'com.github.mfietz:fyydlin:v0.5.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation 'com.github.skydoves:balloon:1.1.5'
// Non-free dependencies:
playImplementation 'com.google.android.play:core:1.8.0'
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"

View File

@ -3,8 +3,10 @@ package de.test.antennapod;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
@ -15,6 +17,9 @@ import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import android.view.View;
import junit.framework.AssertionFailedError;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -33,6 +38,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.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
@ -57,7 +63,7 @@ public class EspressoTestUtils {
@Override
public String getDescription() {
return "wait for a specific view for" + millis + " millis.";
return "wait for a specific view for " + millis + " millis.";
}
@Override
@ -87,6 +93,33 @@ public class EspressoTestUtils {
};
}
/**
* Wait until a certain view becomes visible, but at the longest until the timeout.
* Unlike {@link #waitForView(Matcher, long)} it doesn't stick to the initial root view.
*
* @param viewMatcher The view to wait for.
* @param timeoutMillis Maximum waiting period in milliseconds.
* @throws Exception Throws an Exception in case of a timeout.
*/
public static void waitForViewGlobally(@NonNull Matcher<View> viewMatcher, long timeoutMillis) throws Exception {
long startTime = System.currentTimeMillis();
long endTime = startTime + timeoutMillis;
do {
try {
onView(viewMatcher).check(matches(isDisplayed()));
// no Exception thrown -> check successful
return;
} catch (NoMatchingViewException | AssertionFailedError ignore) {
// check was not successful "not found" -> continue waiting
}
//noinspection BusyWait
Thread.sleep(50);
} while (System.currentTimeMillis() < endTime);
throw new Exception("Timeout after " + timeoutMillis + " ms");
}
/**
* Perform action of waiting for a specific view id.
* https://stackoverflow.com/a/30338665/
@ -113,7 +146,7 @@ public class EspressoTestUtils {
}
/**
* Clear all app databases
* Clear all app databases.
*/
public static void clearPreferences() {
File root = InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir().getParentFile();

View File

@ -11,15 +11,11 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.fragment.EpisodesFragment;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.ui.UITestUtils;
@ -70,7 +66,6 @@ public class ShareDialogTest {
onView(withText(R.string.all_episodes_short_label)).perform(click());
Matcher<View> allEpisodesMatcher;
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10);
allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2));
onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000));
onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click()));

View File

@ -1,43 +0,0 @@
package de.test.antennapod.feed;
import androidx.test.filters.SmallTest;
import de.danoeh.antennapod.core.feed.FeedItem;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@SmallTest
public class FeedItemTest {
private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
private static final String TEXT_SHORT = "Lorem ipsum";
/**
* If one of `description` or `content:encoded` is null, use the other one.
*/
@Test
public void testShownotesNullValues() throws Exception {
testShownotes(null, TEXT_LONG);
testShownotes(TEXT_LONG, null);
}
/**
* If `description` is reasonably longer than `content:encoded`, use `description`.
*/
@Test
public void testShownotesLength() throws Exception {
testShownotes(TEXT_SHORT, TEXT_LONG);
testShownotes(TEXT_LONG, TEXT_SHORT);
}
/**
* Checks if the shownotes equal TEXT_LONG, using the given `description` and `content:encoded`
* @param description Description of the feed item
* @param contentEncoded `content:encoded` of the feed item
*/
private void testShownotes(String description, String contentEncoded) throws Exception {
FeedItem item = new FeedItem();
item.setDescription(description);
item.setContentEncoded(contentEncoded);
assertEquals(TEXT_LONG, item.loadShownotes().call());
}
}

View File

@ -1,40 +0,0 @@
package de.test.antennapod.handler;
import androidx.test.filters.SmallTest;
import de.danoeh.antennapod.core.feed.Feed;
import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator;
import org.junit.Test;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
/**
* Tests for Atom feeds in FeedHandler.
*/
@SmallTest
public class AtomParserTest extends FeedParserTestBase {
@Test
public void testAtomBasic() throws Exception {
Feed f1 = createTestFeed(10, true);
Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0);
feedValid(f1, f2, Feed.TYPE_ATOM1);
}
@Test
public void testLogoWithWhitespace() throws Exception {
String logo = "https://example.com/image.png";
Feed f1 = createTestFeed(0, false);
f1.setImageUrl(null);
Feed f2 = runFeedTest(f1, new AtomGenerator() {
@Override
protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException {
xml.startTag(null, "logo");
xml.text(" " + logo + "\n");
xml.endTag(null, "logo");
}
}, "UTF-8", 0);
assertEquals(logo, f2.getImageUrl());
}
}

View File

@ -1,154 +0,0 @@
package de.test.antennapod.handler;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException;
import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator;
import org.junit.After;
import org.junit.Before;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for FeedHandler.
*/
public abstract class FeedParserTestBase {
private static final String FEEDS_DIR = "testfeeds";
private File file = null;
private OutputStream outputStream = null;
@Before
public void setUp() throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
File destDir = context.getExternalFilesDir(FEEDS_DIR);
assertNotNull(destDir);
file = new File(destDir, "feed.xml");
file.delete();
assertNotNull(file);
assertFalse(file.exists());
outputStream = new FileOutputStream(file);
}
@After
public void tearDown() throws Exception {
file.delete();
file = null;
outputStream.close();
outputStream = null;
}
protected Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags)
throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException {
g.writeFeed(feed, outputStream, encoding, flags);
FeedHandler handler = new FeedHandler();
Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate());
parsedFeed.setFile_url(file.getAbsolutePath());
parsedFeed.setDownloaded(true);
handler.parseFeed(parsedFeed);
return parsedFeed;
}
protected void feedValid(Feed feed, Feed parsedFeed, String feedType) {
assertEquals(feed.getTitle(), parsedFeed.getTitle());
if (feedType.equals(Feed.TYPE_ATOM1)) {
assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier());
} else {
assertEquals(feed.getLanguage(), parsedFeed.getLanguage());
}
assertEquals(feed.getLink(), parsedFeed.getLink());
assertEquals(feed.getDescription(), parsedFeed.getDescription());
assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink());
assertEquals(feed.getImageUrl(), parsedFeed.getImageUrl());
if (feed.getItems() != null) {
assertNotNull(parsedFeed.getItems());
assertEquals(feed.getItems().size(), parsedFeed.getItems().size());
for (int i = 0; i < feed.getItems().size(); i++) {
FeedItem item = feed.getItems().get(i);
FeedItem parsedItem = parsedFeed.getItems().get(i);
if (item.getItemIdentifier() != null) {
assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier());
}
assertEquals(item.getTitle(), parsedItem.getTitle());
assertEquals(item.getDescription(), parsedItem.getDescription());
assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded());
assertEquals(item.getLink(), parsedItem.getLink());
assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime());
assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink());
if (item.hasMedia()) {
assertTrue(parsedItem.hasMedia());
FeedMedia media = item.getMedia();
FeedMedia parsedMedia = parsedItem.getMedia();
assertEquals(media.getDownload_url(), parsedMedia.getDownload_url());
assertEquals(media.getSize(), parsedMedia.getSize());
assertEquals(media.getMime_type(), parsedMedia.getMime_type());
}
assertEquals(feed.getImageUrl(), item.getImageLocation());
if (item.getChapters() != null) {
assertNotNull(parsedItem.getChapters());
assertEquals(item.getChapters().size(), parsedItem.getChapters().size());
List<Chapter> chapters = item.getChapters();
List<Chapter> parsedChapters = parsedItem.getChapters();
for (int j = 0; j < chapters.size(); j++) {
Chapter chapter = chapters.get(j);
Chapter parsedChapter = parsedChapters.get(j);
assertEquals(chapter.getTitle(), parsedChapter.getTitle());
assertEquals(chapter.getLink(), parsedChapter.getLink());
}
}
}
}
}
protected Feed createTestFeed(int numItems, boolean withFeedMedia) {
Feed feed = new Feed(0, null, "title", "http://example.com", "This is the description",
"http://example.com/payment", "Daniel", "en", null, "http://example.com/feed",
"http://example.com/picture", file.getAbsolutePath(), "http://example.com/feed", true);
feed.setItems(new ArrayList<>());
for (int i = 0; i < numItems; i++) {
FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i,
"http://example.com/items/" + i, new Date(i * 60000), FeedItem.UNPLAYED, feed);
feed.getItems().add(item);
if (withFeedMedia) {
item.setMedia(new FeedMedia(0, item, 4711, 0, 1024 * 1024, "audio/mp3", null,
"http://example.com/media-" + i, false, null, 0, 0));
}
}
return feed;
}
}

View File

@ -1,63 +0,0 @@
package de.test.antennapod.handler;
import androidx.test.filters.SmallTest;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.syndication.namespace.NSMedia;
import de.test.antennapod.util.syndication.feedgenerator.Rss2Generator;
import org.junit.Test;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
/**
* Tests for RSS feeds in FeedHandler.
*/
@SmallTest
public class RssParserTest extends FeedParserTestBase {
@Test
public void testRss2Basic() throws Exception {
Feed f1 = createTestFeed(10, true);
Feed f2 = runFeedTest(f1, new Rss2Generator(), "UTF-8", Rss2Generator.FEATURE_WRITE_GUID);
feedValid(f1, f2, Feed.TYPE_RSS2);
}
@Test
public void testImageWithWhitespace() throws Exception {
String image = "https://example.com/image.png";
Feed f1 = createTestFeed(0, false);
f1.setImageUrl(null);
Feed f2 = runFeedTest(f1, new Rss2Generator() {
@Override
protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException {
xml.startTag(null, "image");
xml.startTag(null, "url");
xml.text(" " + image + "\n");
xml.endTag(null, "url");
xml.endTag(null, "image");
}
}, "UTF-8", 0);
assertEquals(image, f2.getImageUrl());
}
@Test
public void testMediaContentMime() throws Exception {
Feed f1 = createTestFeed(0, false);
f1.setImageUrl(null);
Feed f2 = runFeedTest(f1, new Rss2Generator() {
@Override
protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException {
xml.setPrefix(NSMedia.NSTAG, NSMedia.NSURI);
xml.startTag(null, "item");
xml.startTag(NSMedia.NSURI, "content");
xml.attribute(null, "url", "https://www.example.com/file.mp4");
xml.attribute(null, "medium", "video");
xml.endTag(NSMedia.NSURI, "content");
xml.endTag(null, "item");
}
}, "UTF-8", 0);
assertEquals(MediaType.VIDEO, f2.getItems().get(0).getMedia().getMediaType());
}
}

View File

@ -10,6 +10,7 @@ import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.core.feed.FeedItemFilter;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.After;
@ -107,7 +108,12 @@ public class PlaybackTest {
}
private void setupPlaybackController() {
controller = new PlaybackController(activityTestRule.getActivity());
controller = new PlaybackController(activityTestRule.getActivity()) {
@Override
public void loadMediaInfo() {
// Do nothing
}
};
controller.init();
}
@ -252,7 +258,7 @@ public class PlaybackTest {
onView(isRoot()).perform(waitForView(withText(R.string.all_episodes_short_label), 1000));
onView(withText(R.string.all_episodes_short_label)).perform(click());
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10);
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered());
Matcher<View> allEpisodesMatcher = allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2));
onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000));
onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton)));
@ -287,7 +293,7 @@ public class PlaybackTest {
uiTestUtils.addLocalFeedData(true);
DBWriter.clearQueue().get();
activityTestRule.launchActivity(new Intent());
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10);
final List<FeedItem> episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered());
startLocalPlayback();
FeedMedia media = episodes.get(0).getMedia();

View File

@ -6,6 +6,7 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import org.awaitility.Awaitility;
import org.greenrobot.eventbus.EventBus;
import org.junit.After;
@ -187,8 +188,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -248,8 +249,9 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
countDownLatch.countDown();
return null;
}
@Override
@ -348,8 +350,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -391,8 +393,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -449,8 +451,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override

View File

@ -3,13 +3,13 @@ package de.test.antennapod.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.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;
@ -29,8 +29,7 @@ public class AutoDownloadTest {
private Context context;
private UITestUtils stubFeedsServer;
private AutomaticDownloadAlgorithm automaticDownloadAlgorithmOrig;
private StubDownloadAlgorithm stubDownloadAlgorithm;
@Before
public void setUp() throws Exception {
@ -39,16 +38,19 @@ public class AutoDownloadTest {
stubFeedsServer = new UITestUtils(context);
stubFeedsServer.setup();
automaticDownloadAlgorithmOrig = ClientConfig.automaticDownloadAlgorithm;
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 {
ClientConfig.automaticDownloadAlgorithm = automaticDownloadAlgorithmOrig;
DBTasks.setDownloadAlgorithm(new AutomaticDownloadAlgorithm());
EspressoTestUtils.tryKillPlaybackService();
stubFeedsServer.tearDown();
}
@ -74,11 +76,6 @@ public class AutoDownloadTest {
FeedItem item0 = queue.get(0);
FeedItem item1 = queue.get(1);
// Setup: enable automatic download
// it is not needed, as the actual automatic download is stubbed.
StubDownloadAlgorithm stubDownloadAlgorithm = new StubDownloadAlgorithm();
ClientConfig.automaticDownloadAlgorithm = stubDownloadAlgorithm;
// Actual test
// Play the first one in the queue
playEpisode(item0);
@ -92,11 +89,10 @@ public class AutoDownloadTest {
} catch (ConditionTimeoutException cte) {
long actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload();
fail("when auto download is triggered, the next episode should be playing: ("
+ item1.getId() + ", " + item1.getTitle() + ") . "
+ item1.getId() + ", " + item1.getTitle() + ") . "
+ "Actual playing: (" + actual + ")"
);
}
}
private void playEpisode(@NonNull FeedItem item) {
@ -111,7 +107,7 @@ public class AutoDownloadTest {
.until(() -> item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId());
}
private static class StubDownloadAlgorithm implements AutomaticDownloadAlgorithm {
private static class StubDownloadAlgorithm extends AutomaticDownloadAlgorithm {
private long currentlyPlaying = -1;
@Override

View File

@ -2,16 +2,14 @@ package de.test.antennapod.ui;
import android.app.Activity;
import android.content.Intent;
import androidx.test.platform.app.InstrumentationRegistry;
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.core.feed.Feed;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.test.antennapod.EspressoTestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@ -20,6 +18,12 @@ import org.junit.runner.RunWith;
import java.io.IOException;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
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;
@ -28,18 +32,17 @@ 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.isRoot;
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.waitForView;
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
* User interface tests for MainActivity.
*/
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@ -48,19 +51,19 @@ public class MainActivityTest {
private UITestUtils uiTestUtils;
@Rule
public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false);
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(MainActivity.class, false, false);
@Before
public void setUp() throws IOException {
EspressoTestUtils.clearPreferences();
EspressoTestUtils.clearDatabase();
mActivityRule.launchActivity(new Intent());
activityRule.launchActivity(new Intent());
uiTestUtils = new UITestUtils(InstrumentationRegistry.getInstrumentation().getTargetContext());
uiTestUtils.setup();
solo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivityRule.getActivity());
solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityRule.getActivity());
}
@After
@ -71,6 +74,7 @@ public class MainActivityTest {
@Test
public void testAddFeed() throws Exception {
// connect to podcast feed
uiTestUtils.addHostedFeedData();
final Feed feed = uiTestUtils.hostedFeeds.get(0);
openNavDrawer();
@ -78,9 +82,14 @@ public class MainActivityTest {
onView(withId(R.id.addViaUrlButton)).perform(scrollTo(), click());
onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownload_url()));
onView(withText(R.string.confirm_label)).perform(scrollTo(), click());
// subscribe podcast
Espresso.closeSoftKeyboard();
waitForViewGlobally(withText(R.string.subscribe_label), 15000);
onView(withText(R.string.subscribe_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(R.id.butShowSettings), 5000));
// wait for podcast feed item list
waitForViewGlobally(withId(R.id.butShowSettings), 15000);
}
@Test
@ -100,7 +109,7 @@ public class MainActivityTest {
onView(allOf(withId(R.id.toolbar), isDisplayed())).check(
matches(hasDescendant(withText(R.string.subscriptions_label))));
solo.goBack();
assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
@ -113,7 +122,7 @@ public class MainActivityTest {
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
assertTrue(((MainActivity)solo.getCurrentActivity()).isDrawerOpen());
assertTrue(((MainActivity) solo.getCurrentActivity()).isDrawerOpen());
}
@Test
@ -127,7 +136,7 @@ public class MainActivityTest {
solo.goBack();
solo.goBack();
solo.goBack();
assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
@ -142,7 +151,7 @@ public class MainActivityTest {
solo.goBack();
onView(withText(R.string.yes)).perform(click());
Thread.sleep(100);
assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
@Test
@ -155,6 +164,6 @@ public class MainActivityTest {
solo.goBackToActivity(MainActivity.class.getSimpleName());
solo.goBack();
solo.goBack();
assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED));
}
}

View File

@ -15,6 +15,7 @@ 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.EpisodesFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
@ -371,6 +372,17 @@ public class PreferencesTest {
.until(() -> enableAutodownloadOnBattery == UserPreferences.isEnableAutodownloadOnBattery());
}
@Test
public void testEpisodeCleanupFavoriteOnly() {
clickPreference(R.string.network_pref);
onView(withText(R.string.pref_automatic_download_title)).perform(click());
onView(withText(R.string.pref_episode_cleanup_title)).perform(click());
onView(isRoot()).perform(waitForView(withText(R.string.episode_cleanup_except_favorite_removal), 1000));
onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click());
Awaitility.await().atMost(1000, MILLISECONDS)
.until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm);
}
@Test
public void testEpisodeCleanupQueueOnly() {
clickPreference(R.string.network_pref);

View File

@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.test.antennapod.EspressoTestUtils;
import de.test.antennapod.IgnoreOnCi;
import org.awaitility.Awaitility;
@ -71,8 +72,13 @@ public class SpeedChangeTest {
UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f));
EspressoTestUtils.tryKillPlaybackService();
activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true));
controller = new PlaybackController(activityRule.getActivity());
activityRule.launchActivity(new Intent().putExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, true));
controller = new PlaybackController(activityRule.getActivity()) {
@Override
public void loadMediaInfo() {
// Do nothing
}
};
controller.init();
controller.getMedia(); // To load media
}

View File

@ -1,131 +0,0 @@
package de.test.antennapod.util.syndication.feedgenerator;
import android.util.Xml;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.util.DateUtils;
/**
* Creates Atom feeds. See FeedGenerator for more information.
*/
public class AtomGenerator implements FeedGenerator {
private static final String NS_ATOM = "http://www.w3.org/2005/Atom";
private static final long FEATURE_USE_RFC3339LOCAL = 1;
@Override
public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException {
if (feed == null) throw new IllegalArgumentException("feed = null");
if (outputStream == null) throw new IllegalArgumentException("outputStream = null");
if (encoding == null) throw new IllegalArgumentException("encoding = null");
XmlSerializer xml = Xml.newSerializer();
xml.setOutput(outputStream, encoding);
xml.startDocument(encoding, null);
xml.startTag(null, "feed");
xml.attribute(null, "xmlns", NS_ATOM);
// Write Feed data
if (feed.getIdentifyingValue() != null) {
xml.startTag(null, "id");
xml.text(feed.getIdentifyingValue());
xml.endTag(null, "id");
}
if (feed.getTitle() != null) {
xml.startTag(null, "title");
xml.text(feed.getTitle());
xml.endTag(null, "title");
}
if (feed.getLink() != null) {
xml.startTag(null, "link");
xml.attribute(null, "rel", "alternate");
xml.attribute(null, "href", feed.getLink());
xml.endTag(null, "link");
}
if (feed.getDescription() != null) {
xml.startTag(null, "subtitle");
xml.text(feed.getDescription());
xml.endTag(null, "subtitle");
}
if (feed.getImageUrl() != null) {
xml.startTag(null, "logo");
xml.text(feed.getImageUrl());
xml.endTag(null, "logo");
}
if (feed.getPaymentLink() != null) {
GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), false);
}
// Write FeedItem data
if (feed.getItems() != null) {
for (FeedItem item : feed.getItems()) {
xml.startTag(null, "entry");
if (item.getIdentifyingValue() != null) {
xml.startTag(null, "id");
xml.text(item.getIdentifyingValue());
xml.endTag(null, "id");
}
if (item.getTitle() != null) {
xml.startTag(null, "title");
xml.text(item.getTitle());
xml.endTag(null, "title");
}
if (item.getLink() != null) {
xml.startTag(null, "link");
xml.attribute(null, "rel", "alternate");
xml.attribute(null, "href", item.getLink());
xml.endTag(null, "link");
}
if (item.getPubDate() != null) {
xml.startTag(null, "published");
if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) {
xml.text(DateUtils.formatRFC3339Local(item.getPubDate()));
} else {
xml.text(DateUtils.formatRFC3339UTC(item.getPubDate()));
}
xml.endTag(null, "published");
}
if (item.getDescription() != null) {
xml.startTag(null, "content");
xml.text(item.getDescription());
xml.endTag(null, "content");
}
if (item.getMedia() != null) {
FeedMedia media = item.getMedia();
xml.startTag(null, "link");
xml.attribute(null, "rel", "enclosure");
xml.attribute(null, "href", media.getDownload_url());
xml.attribute(null, "type", media.getMime_type());
xml.attribute(null, "length", String.valueOf(media.getSize()));
xml.endTag(null, "link");
}
if (item.getPaymentLink() != null) {
GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), false);
}
xml.endTag(null, "entry");
}
}
writeAdditionalAttributes(xml);
xml.endTag(null, "feed");
xml.endDocument();
}
protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException {
}
}

View File

@ -60,7 +60,8 @@
<activity
android:name=".activity.SplashActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden|orientation|screenSize">
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -83,11 +84,39 @@
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation"
android:windowSoftInputMode="stateAlwaysHidden"
android:launchMode="singleTask"
android:label="@string/app_name">
android:label="@string/app_name"
android:exported="true">
<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/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" />
</intent-filter>
</activity>
<activity
android:name=".activity.DownloadAuthenticationActivity"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:launchMode="singleInstance"/>
<activity
@ -101,7 +130,8 @@
<activity
android:name=".activity.WidgetConfigActivity"
android:label="@string/widget_settings">
android:label="@string/widget_settings"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGUR"/>
</intent-filter>
@ -114,21 +144,21 @@
android:exported="false">
</service>
<receiver android:name=".core.receiver.PlayerWidget">
<receiver
android:name=".core.receiver.PlayerWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<intent-filter>
<action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/player_widget_info"/>
<intent-filter>
<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.StorageErrorActivity">
@ -136,7 +166,8 @@
<activity
android:name=".activity.OpmlImportActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/opml_import_label">
android:label="@string/opml_import_label"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@ -185,18 +216,14 @@
android:name=".activity.VideoplayerActivity"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:supportsPictureInPicture="true"
android:screenOrientation="sensorLandscape">
android:screenOrientation="sensorLandscape"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.MainActivity"/>
<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:scheme="file"/>
<data android:mimeType="video/*"/>
<action android:name="de.danoeh.antennapod.intents.VIDEO_PLAYER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@ -204,7 +231,8 @@
android:name=".activity.OnlineFeedViewActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.AntennaPod.Dark.Translucent"
android:label="@string/add_feed_label">
android:label="@string/add_feed_label"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.MainActivity"/>
@ -292,33 +320,26 @@
</activity>
<activity
android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"
android:configChanges="orientation"
android:label="@string/gpodnet_auth_label">
<intent-filter>
<action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
</activity>
<receiver android:name=".receiver.ConnectivityActionReceiver">
<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">
<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">
<receiver
android:name=".receiver.SPAReceiver"
android:exported="true">
<intent-filter>
<action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/>
</intent-filter>
@ -333,6 +354,10 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
<meta-data
android:name="com.google.android.actions"
android:resource="@xml/actions" />
</application>
</manifest>

View File

@ -4,14 +4,14 @@ mfietz;6860662;Maintainer (retired)
TomHennen;5216560;Maintainer (retired)
orionlee;250644;Contributor
domingos86;9538859;Contributor
damoasda;46045854;Contributor
tonytamsf;149837;Contributor
andersonvom;69922;Contributor
damoasda;46045854;Contributor
TacoTheDank;32376686;Contributor
shortspider;5712543;Contributor
spacecowboy;223655;Contributor
ebraminio;833473;Contributor
asdoi;36813904;Contributor
spacecowboy;223655;Contributor
patheticpat;16046;Contributor
brad;1614;Contributor
Cj-Malone;10121513;Contributor
@ -44,6 +44,7 @@ mchelen;30691;Contributor
dethstar;1239177;Contributor
drabux;10663142;Contributor
saqura;1935380;Contributor
binarytoto;75904760;Contributor
bibz;5141956;Contributor
hzulla;1705654;Contributor
deandreamatias;21011641;Contributor
@ -65,6 +66,7 @@ HrBDev;25826502;Contributor
HolgerJeromin;2410353;Contributor
xisberto;1914956;Contributor
jmue;898577;Contributor
jonasburian;15125616;Contributor
katrinleinweber;9948149;Contributor
LatinSuD;451487;Contributor
24hours;650407;Contributor
@ -74,25 +76,27 @@ 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
hannesaa2;18496079;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
Thom-Merrilin;76849828;Contributor
wseemann;2296196;Contributor
markamaze;17114678;Contributor
mohitshah3111999;42018918;Contributor
moralesg;14352147;Contributor
mr-intj;6268767;Contributor
tuxayo;2678215;Contributor
schlch;56929215;Contributor
alimemonzx;44647595;Contributor
dev-darrell;52300159;Contributor
jmdouglas;10855634;Contributor
@ -106,24 +110,27 @@ arantius;84729;Contributor
BoJacobs;25435640;Contributor
chetan882777;36985543;Contributor
chrissicool;232590;Contributor
britiger;2057760;Contributor
cszucko;1810383;Contributor
CWftw;1498303;Contributor
connectety;26038710;Contributor
danielm5;66779;Contributor
ariedov;958646;Contributor
brettle;118192;Contributor
edwinhere;19705425;Contributor
eirikv;4076243;Contributor
eerden;277513;Contributor
Geist5000;37940313;Contributor
jklippel;8657220;Contributor
jannic;232606;Contributor
Foso;5015532;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
msoose;30473690;Contributor
mo;7117;Contributor
mdeveloper20;2319126;Contributor
@ -138,6 +145,7 @@ ortylp;470439;Contributor
ramzan;55637406;Contributor
iamrichR;44210678;Contributor
SamWhited;512573;Contributor
SebiderSushi;23618858;Contributor
selivan;1208989;Contributor
sonnayasomnambula;7716779;Contributor
sethoscope;534043;Contributor
@ -148,14 +156,17 @@ vimsick;20211590;Contributor
lyallemma;25173082;Contributor
edent;837136;Contributor
atrus6;357881;Contributor
timakro;8438790;Contributor
heyyviv;56256802;Contributor
waylife;3348620;Contributor
yarons;406826;Contributor
amhokies;3124968;Contributor
andrewc1;19559401;Contributor
axq;5077221;Contributor
binarytoto;75904760;Contributor
chrk2205;44704035;Contributor
fossterer;4236021;Contributor
lightonflux;1377943;Contributor
minusf;3632883;Contributor
s3lph;5564491;Contributor
tamizh138;26201258;Contributor
zawad2221;32180355;Contributor

1 ByteHamster 5811634 Maintainer
4 TomHennen 5216560 Maintainer (retired)
5 orionlee 250644 Contributor
6 domingos86 9538859 Contributor
7 damoasda 46045854 Contributor
8 tonytamsf 149837 Contributor
9 andersonvom 69922 Contributor
damoasda 46045854 Contributor
10 TacoTheDank 32376686 Contributor
11 shortspider 5712543 Contributor
12 spacecowboy 223655 Contributor
13 ebraminio 833473 Contributor
14 asdoi 36813904 Contributor
spacecowboy 223655 Contributor
15 patheticpat 16046 Contributor
16 brad 1614 Contributor
17 Cj-Malone 10121513 Contributor
44 dethstar 1239177 Contributor
45 drabux 10663142 Contributor
46 saqura 1935380 Contributor
47 binarytoto 75904760 Contributor
48 bibz 5141956 Contributor
49 hzulla 1705654 Contributor
50 deandreamatias 21011641 Contributor
66 HolgerJeromin 2410353 Contributor
67 xisberto 1914956 Contributor
68 jmue 898577 Contributor
69 jonasburian 15125616 Contributor
70 katrinleinweber 9948149 Contributor
71 LatinSuD 451487 Contributor
72 24hours 650407 Contributor
76 alifeflow 24603829 Contributor
77 avirajrsingh 69088913 Contributor
78 toggles 14695 Contributor
79 connectety 26038710 Contributor
80 matdb 48329535 Contributor
81 damlayildiz 56313500 Contributor
82 kingargyle 177042 Contributor
83 dsmith47 14109426 Contributor
84 hannesaa2 18496079 Contributor
85 jhunnius 9149031 Contributor
86 a1291762 327162 Contributor
87 ShadowIce 59123 Contributor
88 Niffler 8172446 Contributor
89 raghulj 57007 Contributor
90 raghulrm 5362986 Contributor
91 mamehacker 16738348 Contributor
92 skitt 2128935 Contributor
93 Thom-Merrilin 76849828 Contributor
94 wseemann 2296196 Contributor
95 markamaze 17114678 Contributor
96 mohitshah3111999 42018918 Contributor
97 moralesg 14352147 Contributor
98 mr-intj 6268767 Contributor
99 tuxayo 2678215 Contributor
schlch 56929215 Contributor
100 alimemonzx 44647595 Contributor
101 dev-darrell 52300159 Contributor
102 jmdouglas 10855634 Contributor
110 BoJacobs 25435640 Contributor
111 chetan882777 36985543 Contributor
112 chrissicool 232590 Contributor
113 britiger 2057760 Contributor
114 cszucko 1810383 Contributor
115 CWftw 1498303 Contributor
connectety 26038710 Contributor
116 danielm5 66779 Contributor
117 ariedov 958646 Contributor
118 brettle 118192 Contributor
119 edwinhere 19705425 Contributor
120 eirikv 4076243 Contributor
121 eerden 277513 Contributor
122 Geist5000 37940313 Contributor
123 jklippel 8657220 Contributor
124 jannic 232606 Contributor
125 Foso 5015532 Contributor
126 Kaligule 3586246 Contributor
127 kvithayathil 1056073 Contributor
128 luiscruz 1080714 Contributor
129 MStrecke 5202211 Contributor
130 mlasson 5814258 Contributor
131 schwedenmut 9077622 Contributor
132 M-arcel 56698158 Contributor
133 mgborowiec 29843126 Contributor
134 msoose 30473690 Contributor
135 mo 7117 Contributor
136 mdeveloper20 2319126 Contributor
145 ramzan 55637406 Contributor
146 iamrichR 44210678 Contributor
147 SamWhited 512573 Contributor
148 SebiderSushi 23618858 Contributor
149 selivan 1208989 Contributor
150 sonnayasomnambula 7716779 Contributor
151 sethoscope 534043 Contributor
156 lyallemma 25173082 Contributor
157 edent 837136 Contributor
158 atrus6 357881 Contributor
159 timakro 8438790 Contributor
160 heyyviv 56256802 Contributor
161 waylife 3348620 Contributor
162 yarons 406826 Contributor
163 amhokies 3124968 Contributor
164 andrewc1 19559401 Contributor
165 axq 5077221 Contributor
binarytoto 75904760 Contributor
166 chrk2205 44704035 Contributor
167 fossterer 4236021 Contributor
168 lightonflux 1377943 Contributor
169 minusf 3632883 Contributor
170 s3lph 5564491 Contributor
171 tamizh138 26201258 Contributor
172 zawad2221 32180355 Contributor

View File

@ -90,12 +90,6 @@
website="https://github.com/square/okio"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="RecyclerView-FlexibleDivider"
author="yqritc"
website="https://github.com/yqritc/RecyclerView-FlexibleDivider"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<library
name="RxAndroid"
author="ReactiveX"

View File

@ -1,19 +1,19 @@
Arabic;abuzar3.khalid, badarotti, keunes, nabilMaghura, rex07, shubbar
Arabic;abuzar3.khalid, badarotti, keunes, MustafaAlgurabi, nabilMaghura, rex07, shubbar
Asturian (ast_ES);enolp
Basque;gaztainalde, keunes, Osoitz, pospolos
Breton;Belvar, keunes
Bulgarian;keunes, solusitor
Bulgarian;keunes, ma4ko, solusitor
Catalan;carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70
Chinese (zh_CN);brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, 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, elich, Hanzmeister, svetlemodry, Thomaash
Danish;JFreak, jhertel, keunes, SebastianKiwiDk, twikedk
Czech (cs_CZ);anotheranonymoususer, elich, Hanzmeister, md.share, svetlemodry, Thomaash
Danish;JFreak, jhertel, keunes, petterbejo, SebastianKiwiDk
Dutch;e2jk, keunes, rwv, Vistaus
Estonian;Eraser, keunes, mahfiaz
Finnish;Ban3, keunes, Sahtor
French;ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, Poussinou, sterylmreep
French;ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, petterbejo, Poussinou, RomainTT, sterylmreep
Galician;antiparvos, pikamoku, Raichely
German;_Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, mfietz, pudeeh, Quiss42, repat, tomte, tweimer, Willhelm, ypid
German;_Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, max.wittig, mfietz, Michael_Strecke, petterbejo, pudeeh, Quiss42, repat, toaskoas, tomte, tweimer, Willhelm, ypid
Modern Greek (1453-);AnimaRain, antonist, keunes, pavlosv
Hebrew (he_IL);amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron
Hindi (hi_IN);keunes, purple.coder, siddhusengar, thelazyoxymoron
@ -24,20 +24,20 @@ Italian (it_IT);aalex70, allin, alvami, Bonnee, dontknowcris, giuseppep, Guybrus
Japanese;keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG
Kannada (kn_IN);chiraag.nataraj, keunes, thejeshgn
Ko;changwoo, keunes, libliboom
Lithuanian;keunes, naglis
Lithuanian;keunes, naglis, Sharper
Macedonian;krisfremen
Malayalam;joice, keunes, rashivkp
Norwegian Bokmål (nb_NO);abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast
Persian;ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas
Polish (pl_PL);befeleme, hiro2020, Iwangelion, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle
Polish (pl_PL);befeleme, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle
Portuguese;emansije, keunes, smarquespt
Portuguese (pt_BR);alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam
Portuguese (pt_BR);alexupits, alysonborges, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam
Romanian (ro_RO);corneliu.e, fuzzmz, keunes, ralienpp
Russian (ru_RU);ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, un_logic, Vladryyu, whereisthetea
Slovak;ati3, keunes, marulinko, tiborepcek
Slovenian (sl_SI);keunes, panter23
Spanish;AleksSyntek, andersonvom, andrespelaezp, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo
Swahili (macrolanguage);keunes, kmtra
Russian (ru_RU);ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea
Slovak;ati3, jose1711, keunes, marulinko, tiborepcek
Slovenian (sl_SI);asovic, keunes, panter23, trus2
Spanish;AleksSyntek, andersonvom, andrespelaezp, Atreyu94, CaeM0R, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo
Swahili (macrolanguage);1silvester, keunes, kmtra
Swedish (sv_SE);bpnilsson, keunes, nilso, TwoD
Telugu;keunes, veeven
Turkish;AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem

1 Arabic abuzar3.khalid, badarotti, keunes, nabilMaghura, rex07, shubbar abuzar3.khalid, badarotti, keunes, MustafaAlgurabi, nabilMaghura, rex07, shubbar
2 Asturian (ast_ES) enolp enolp
3 Basque gaztainalde, keunes, Osoitz, pospolos gaztainalde, keunes, Osoitz, pospolos
4 Breton Belvar, keunes Belvar, keunes
5 Bulgarian keunes, solusitor keunes, ma4ko, solusitor
6 Catalan carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70 carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70
7 Chinese (zh_CN) brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok
8 Chinese (zh_TW) bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808 bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808
9 Czech (cs_CZ) anotheranonymoususer, elich, Hanzmeister, svetlemodry, Thomaash anotheranonymoususer, elich, Hanzmeister, md.share, svetlemodry, Thomaash
10 Danish JFreak, jhertel, keunes, SebastianKiwiDk, twikedk JFreak, jhertel, keunes, petterbejo, SebastianKiwiDk
11 Dutch e2jk, keunes, rwv, Vistaus e2jk, keunes, rwv, Vistaus
12 Estonian Eraser, keunes, mahfiaz Eraser, keunes, mahfiaz
13 Finnish Ban3, keunes, Sahtor Ban3, keunes, Sahtor
14 French ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, Poussinou, sterylmreep ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, petterbejo, Poussinou, RomainTT, sterylmreep
15 Galician antiparvos, pikamoku, Raichely antiparvos, pikamoku, Raichely
16 German _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, mfietz, pudeeh, Quiss42, repat, tomte, tweimer, Willhelm, ypid _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, max.wittig, mfietz, Michael_Strecke, petterbejo, pudeeh, Quiss42, repat, toaskoas, tomte, tweimer, Willhelm, ypid
17 Modern Greek (1453-) AnimaRain, antonist, keunes, pavlosv AnimaRain, antonist, keunes, pavlosv
18 Hebrew (he_IL) amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron
19 Hindi (hi_IN) keunes, purple.coder, siddhusengar, thelazyoxymoron keunes, purple.coder, siddhusengar, thelazyoxymoron
24 Japanese keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG
25 Kannada (kn_IN) chiraag.nataraj, keunes, thejeshgn chiraag.nataraj, keunes, thejeshgn
26 Ko changwoo, keunes, libliboom changwoo, keunes, libliboom
27 Lithuanian keunes, naglis keunes, naglis, Sharper
28 Macedonian krisfremen krisfremen
29 Malayalam joice, keunes, rashivkp joice, keunes, rashivkp
30 Norwegian Bokmål (nb_NO) abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast
31 Persian ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas
32 Polish (pl_PL) befeleme, hiro2020, Iwangelion, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle befeleme, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle
33 Portuguese emansije, keunes, smarquespt emansije, keunes, smarquespt
34 Portuguese (pt_BR) alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam alexupits, alysonborges, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam
35 Romanian (ro_RO) corneliu.e, fuzzmz, keunes, ralienpp corneliu.e, fuzzmz, keunes, ralienpp
36 Russian (ru_RU) ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, un_logic, Vladryyu, whereisthetea ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea
37 Slovak ati3, keunes, marulinko, tiborepcek ati3, jose1711, keunes, marulinko, tiborepcek
38 Slovenian (sl_SI) keunes, panter23 asovic, keunes, panter23, trus2
39 Spanish AleksSyntek, andersonvom, andrespelaezp, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo AleksSyntek, andersonvom, andrespelaezp, Atreyu94, CaeM0R, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo
40 Swahili (macrolanguage) keunes, kmtra 1silvester, keunes, kmtra
41 Swedish (sv_SE) bpnilsson, keunes, nilso, TwoD bpnilsson, keunes, nilso, TwoD
42 Telugu keunes, veeven keunes, veeven
43 Turkish AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem

View File

@ -1,95 +1,76 @@
package de.danoeh.antennapod.activity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import org.apache.commons.lang3.Validate;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.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.core.storage.DownloadRequester;
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.
* Other arguments are optional.
* The activity's result will be the same DownloadRequest with the entered username and password.
*/
public class DownloadAuthenticationActivity extends AppCompatActivity {
/**
* The download request object that contains information about the resource that requires a username and a password
* 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";
/**
* True if the request should be sent to the DownloadRequester when this activity is finished, false otherwise.
* The default value is false.
*/
public static final String ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL = "send_to_downloadrequester";
private static final String RESULT_REQUEST = "request";
private EditText etxtUsername;
private EditText etxtPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getNoTitleTheme());
setTheme(UserPreferences.getTranslucentTheme());
super.onCreate(savedInstanceState);
setContentView(R.layout.download_authentication_activity);
TextView txtvDescription = findViewById(R.id.txtvDescription);
etxtUsername = findViewById(R.id.etxtUsername);
etxtPassword = findViewById(R.id.etxtPassword);
Button butConfirm = findViewById(R.id.butConfirm);
Button butCancel = findViewById(R.id.butCancel);
Validate.isTrue(getIntent().hasExtra(ARG_DOWNLOAD_REQUEST), "Download request missing");
DownloadRequest request = getIntent().getParcelableExtra(ARG_DOWNLOAD_REQUEST);
boolean sendToDownloadRequester = getIntent().getBooleanExtra(ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, false);
String newDescription = txtvDescription.getText() + ":\n\n" + request.getTitle();
txtvDescription.setText(newDescription);
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 (savedInstanceState != null) {
etxtUsername.setText(savedInstanceState.getString("username"));
etxtPassword.setText(savedInstanceState.getString("password"));
}
butConfirm.setOnClickListener(v -> {
String username = etxtUsername.getText().toString();
String password = etxtPassword.getText().toString();
request.setUsername(username);
request.setPassword(password);
Intent result = new Intent();
result.putExtra(RESULT_REQUEST, request);
setResult(Activity.RESULT_OK, result);
if (sendToDownloadRequester) {
DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request);
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(() -> {
DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request);
finish();
});
}
finish();
});
butCancel.setOnClickListener(v -> {
setResult(Activity.RESULT_CANCELED);
finish();
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("username", etxtUsername.getText().toString());
outState.putString("password", etxtPassword.getText().toString());
@Override
protected void onCancelled() {
finish();
}
}.show();
}
}

View File

@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@ -40,7 +41,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
@ -51,9 +52,11 @@ 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.view.LockableBottomSheetBehavior;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
@ -75,7 +78,6 @@ public class MainActivity extends CastEnabledActivity {
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_OPEN_PLAYER = "open_player";
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 KEY_GENERATED_VIEW_ID = "generated_view_id";
@ -184,16 +186,16 @@ public class MainActivity extends CastEnabledActivity {
}
};
public void setupToolbarToggle(@Nullable Toolbar toolbar) {
public void setupToolbarToggle(@NonNull Toolbar toolbar, boolean displayUpArrow) {
if (drawerLayout != null) { // Tablet layout does not have a drawer
drawerLayout.removeDrawerListener(drawerToggle);
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,
R.string.drawer_open, R.string.drawer_close);
drawerLayout.addDrawerListener(drawerToggle);
drawerToggle.syncState();
drawerToggle.setDrawerIndicatorEnabled(getSupportFragmentManager().getBackStackEntryCount() == 0);
drawerToggle.setDrawerIndicatorEnabled(!displayUpArrow);
drawerToggle.setToolbarNavigationClickListener(v -> getSupportFragmentManager().popBackStack());
} else if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
} else if (!displayUpArrow) {
toolbar.setNavigationIcon(null);
} else {
toolbar.setNavigationIcon(ThemeUtils.getDrawableFromAttr(this, R.attr.homeAsUpIndicator));
@ -508,9 +510,11 @@ public class MainActivity extends CastEnabledActivity {
}
}
sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (intent.getBooleanExtra(EXTRA_OPEN_PLAYER, false)) {
} 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())) {
handleDeeplink(intent.getData());
}
// to avoid handling the intent twice when the configuration changes
setIntent(new Intent(MainActivity.this, MainActivity.class));
@ -520,6 +524,7 @@ public class MainActivity extends CastEnabledActivity {
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleNavIntent();
}
public Snackbar showSnackbarAbovePlayer(CharSequence text, int duration) {
@ -540,6 +545,59 @@ public class MainActivity extends CastEnabledActivity {
return showSnackbarAbovePlayer(getResources().getText(text), duration);
}
/**
* Handles the deep link incoming via App Actions.
* Performs an in-app search or opens the relevant feature of the app
* depending on the query.
*
* @param uri incoming deep link
*/
private void handleDeeplink(Uri uri) {
if (uri == null || uri.getPath() == null) {
return;
}
Log.d(TAG, "Handling deeplink: " + uri.toString());
switch (uri.getPath()) {
case "/deeplink/search":
String query = uri.getQueryParameter("query");
if (query == null) {
return;
}
this.loadChildFragment(SearchFragment.newInstance(query));
break;
case "/deeplink/main":
String feature = uri.getQueryParameter("page");
if (feature == null) {
return;
}
switch (feature) {
case "DOWNLOADS":
loadFragment(DownloadsFragment.TAG, null);
break;
case "HISTORY":
loadFragment(PlaybackHistoryFragment.TAG, null);
break;
case "EPISODES":
loadFragment(EpisodesFragment.TAG, null);
break;
case "QUEUE":
loadFragment(QueueFragment.TAG, null);
break;
case "SUBSCRIPTIONS":
loadFragment(SubscriptionFragment.TAG, null);
break;
default:
showSnackbarAbovePlayer(getString(R.string.app_action_not_found, feature),
Snackbar.LENGTH_LONG);
return;
}
break;
default:
break;
}
}
//Hardware keyboard support
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
@ -592,5 +650,4 @@ public class MainActivity extends CastEnabledActivity {
}
return super.onKeyUp(keyCode, event);
}
}

View File

@ -1,10 +1,8 @@
package de.danoeh.antennapod.activity;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Bundle;
@ -17,7 +15,6 @@ import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
@ -28,17 +25,16 @@ import org.greenrobot.eventbus.ThreadMode;
import java.text.NumberFormat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.cardview.widget.CardView;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
@ -50,11 +46,9 @@ import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.dialog.PlaybackControlsDialog;
import de.danoeh.antennapod.dialog.ShareDialog;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
@ -64,7 +58,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Provides general features which are both needed for playing audio and video
* files.
@ -72,9 +65,6 @@ import io.reactivex.schedulers.Schedulers;
public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener {
private static final String TAG = "MediaplayerActivity";
private static final String PREFS = "MediaPlayerActivityPreferences";
private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
private static final int REQUEST_CODE_STORAGE_PLAY_VIDEO = 42;
private static final int REQUEST_CODE_STORAGE_PLAY_AUDIO = 43;
PlaybackController controller;
@ -87,6 +77,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
private ImageButton butFF;
private TextView txtvFF;
private ImageButton butSkip;
private CardView cardViewSeek;
private TextView txtvSeek;
private boolean showTimeLeft = false;
@ -96,12 +88,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
private PlaybackController newPlaybackController() {
return new PlaybackController(this) {
@Override
public void setupGUI() {
MediaplayerActivity.this.setupGUI();
}
@Override
public void onPositionObserverUpdate() {
MediaplayerActivity.this.onPositionObserverUpdate();
@ -143,8 +129,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}
@Override
public boolean loadMediaInfo() {
return MediaplayerActivity.this.loadMediaInfo();
public void loadMediaInfo() {
MediaplayerActivity.this.loadMediaInfo();
}
@Override
@ -467,17 +453,15 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
* to the PlaybackService to ensure that the activity has the right
* FeedMedia object.
*/
boolean loadMediaInfo() {
void loadMediaInfo() {
Log.d(TAG, "loadMediaInfo()");
if(controller == null || controller.getMedia() == null) {
return false;
if (controller == null || controller.getMedia() == null) {
return;
}
SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
showTimeLeft = UserPreferences.shouldShowRemainingTime();
onPositionObserverUpdate();
checkFavorite();
updatePlaybackSpeedButton();
return true;
}
void updatePlaybackSpeedButton() {
@ -492,9 +476,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
setContentView(getContentViewResourceId());
sbPosition = findViewById(R.id.sbPosition);
txtvPosition = findViewById(R.id.txtvPosition);
cardViewSeek = findViewById(R.id.cardViewSeek);
txtvSeek = findViewById(R.id.txtvSeek);
SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
showTimeLeft = UserPreferences.shouldShowRemainingTime();
Log.d("timeleft", showTimeLeft ? "true" : "false");
txtvLength = findViewById(R.id.txtvLength);
if (txtvLength != null) {
@ -518,9 +504,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}
txtvLength.setText(length);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft);
editor.apply();
UserPreferences.setShowRemainTimeSetting(showTimeLeft);
Log.d("timeleft on click", showTimeLeft ? "true" : "false");
});
}
@ -618,21 +602,21 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}
if (fromUser) {
prog = progress / ((float) seekBar.getMax());
int duration = controller.getDuration();
TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
int position = converter.convert((int) (prog * duration));
txtvPosition.setText(Converter.getDurationStringLong(position));
if (showTimeLeft) {
int timeLeft = converter.convert(duration - (int) (prog * duration));
txtvLength.setText("-" + Converter.getDurationStringLong(timeLeft));
}
int position = converter.convert((int) (prog * controller.getDuration()));
txtvSeek.setText(Converter.getDurationStringLong(position));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
cardViewSeek.setScaleX(.8f);
cardViewSeek.setScaleY(.8f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start();
}
@Override
@ -640,6 +624,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
if (controller != null) {
controller.seekTo((int) (prog * controller.getDuration()));
}
cardViewSeek.setScaleX(1f);
cardViewSeek.setScaleY(1f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start();
}
private void checkFavorite() {
@ -663,50 +654,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
void playExternalMedia(Intent intent, MediaType type) {
if (intent == null || intent.getData() == null) {
return;
}
if (Build.VERSION.SDK_INT >= 23
&& ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show();
}
int code = REQUEST_CODE_STORAGE_PLAY_AUDIO;
if (type == MediaType.VIDEO) {
code = REQUEST_CODE_STORAGE_PLAY_VIDEO;
}
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, code);
return;
}
Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath());
ExternalMedia media = new ExternalMedia(intent.getData().getPath(), type);
new PlaybackServiceStarter(this, media)
.callEvenIfRunning(true)
.startWhenPrepared(true)
.shouldStream(false)
.prepareImmediately(true)
.start();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_CODE_STORAGE_PLAY_AUDIO) {
playExternalMedia(getIntent(), MediaType.AUDIO);
} else if (requestCode == REQUEST_CODE_STORAGE_PLAY_VIDEO) {
playExternalMedia(getIntent(), MediaType.VIDEO);
}
} else {
Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show();
}
}
@Nullable
private static FeedItem getFeedItem(@Nullable Playable playable) {
if (playable instanceof FeedMedia) {

View File

@ -4,10 +4,14 @@ 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.Build;
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;
@ -15,6 +19,7 @@ import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@ -28,7 +33,6 @@ import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
@ -41,6 +45,7 @@ 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.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.syndication.handler.FeedHandler;
@ -49,7 +54,6 @@ import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeExceptio
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.Optional;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.playback.RemoteMedia;
@ -58,9 +62,11 @@ import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.discovery.PodcastSearcherRegistry;
import 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;
@ -87,6 +93,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
// 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 Feed feed;
private String selectedDownloadUrl;
@ -248,7 +257,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
url = URLChecker.prepareURL(url);
feed = new Feed(url, null);
if (username != null && password != null) {
feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password));
feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL,
VolumeAdaptionSetting.OFF, username, password));
}
String fileUrl = new File(getExternalCacheDir(),
FileNameGenerator.generateFileName(feed.getDownload_url())).toString();
@ -283,11 +293,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
dialog.show();
}
} else {
String errorMsg = status.getReason().getErrorString(OnlineFeedViewActivity.this);
if (status.getReasonDetailed() != null) {
errorMsg += " (" + status.getReasonDetailed() + ")";
}
showErrorDialog(errorMsg);
showErrorDialog(status.getReason().getErrorString(OnlineFeedViewActivity.this), status.getReasonDetailed());
}
}
@ -316,37 +322,47 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
}
Log.d(TAG, "Parsing feed");
parser = Observable.fromCallable(this::doParseFeed)
parser = Maybe.fromCallable(this::doParseFeed)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(optionalResult -> {
if(optionalResult.isPresent()) {
FeedHandlerResult result = optionalResult.get();
beforeShowFeedInformation(result.feed);
.subscribeWith(new DisposableMaybeObserver<FeedHandlerResult>() {
@Override
public void onSuccess(@NonNull FeedHandlerResult result) {
showFeedInformation(result.feed, result.alternateFeedUrls);
}
}, error -> {
String errorMsg = DownloadError.ERROR_PARSER_EXCEPTION.getErrorString(
OnlineFeedViewActivity.this) + " (" + error.getMessage() + ")";
showErrorDialog(errorMsg);
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error));
@Override
public void onComplete() {
// Ignore null result: We showed the discovery dialog.
}
@Override
public void onError(@NonNull Throwable error) {
showErrorDialog(error.getMessage(), "");
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error));
}
});
}
@NonNull
private Optional<FeedHandlerResult> doParseFeed() throws Exception {
/**
* Try to parse the feed.
* @return The FeedHandlerResult if successful.
* Null if unsuccessful but we started another attempt.
* @throws Exception If unsuccessful but we do not know a resolution.
*/
@Nullable
private FeedHandlerResult doParseFeed() throws Exception {
FeedHandler handler = new FeedHandler();
try {
return Optional.of(handler.parseFeed(feed));
return handler.parseFeed(feed);
} catch (UnsupportedFeedtypeException e) {
Log.d(TAG, "Unsupported feed type detected");
if ("html".equalsIgnoreCase(e.getRootElement())) {
boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url());
if (dialogShown) {
return Optional.empty();
return null; // Should not display an error message
} else {
Log.d(TAG, "Supplied feed is an HTML web page that has no references to any feed");
throw e;
throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html));
}
} else {
throw e;
@ -360,23 +376,6 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
}
}
/**
* Called after the feed has been downloaded and parsed and before showFeedInformation is called.
* This method is executed on a background thread
*/
private void beforeShowFeedInformation(Feed feed) {
Log.d(TAG, "Removing HTML from feed description");
feed.setDescription(HtmlToPlainText.getPlainText(feed.getDescription()));
Log.d(TAG, "Removing HTML from shownotes");
if (feed.getItems() != null) {
for (FeedItem item : feed.getItems()) {
item.setDescription(HtmlToPlainText.getPlainText(item.getDescription()));
}
}
}
/**
* Called when feed parsed successfully.
* This method is executed on the GUI thread.
@ -420,7 +419,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
viewBinding.titleLabel.setText(feed.getTitle());
viewBinding.authorLabel.setText(feed.getAuthor());
description.setText(feed.getDescription());
description.setText(HtmlToPlainText.getPlainText(feed.getDescription()));
viewBinding.subscribeButton.setOnClickListener(v -> {
if (feedInFeedlist(feed)) {
@ -445,6 +444,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
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 -> {
@ -511,10 +515,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
if (didPressSubscribe) {
didPressSubscribe = false;
if (UserPreferences.isEnableAutodownload()) {
boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked();
Feed feed1 = DBReader.getFeed(getFeedId(feed));
FeedPreferences feedPreferences = feed1.getPreferences();
feedPreferences.setAutoDownload(viewBinding.autoDownloadCheckBox.isChecked());
feed1.savePreferences();
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();
}
@ -553,12 +564,16 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
}
@UiThread
private void showErrorDialog(String errorMsg) {
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) {
builder.setMessage(errorMsg);
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);
}

View File

@ -17,7 +17,6 @@ import android.widget.ImageView;
import androidx.core.view.WindowCompat;
import androidx.appcompat.app.ActionBar;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
@ -37,12 +36,12 @@ import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.view.AspectRatioVideoView;
/**
@ -88,9 +87,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void onResume() {
super.onResume();
if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
playExternalMedia(getIntent(), MediaType.VIDEO);
} else if (PlaybackService.isCasting()) {
if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {
destroyingDueToReload = true;
@ -135,17 +132,13 @@ public class VideoplayerActivity extends MediaplayerActivity {
}
@Override
protected boolean loadMediaInfo() {
if (!super.loadMediaInfo() || controller == null) {
return false;
}
protected void loadMediaInfo() {
super.loadMediaInfo();
Playable media = controller.getMedia();
if (media != null) {
getSupportActionBar().setSubtitle(media.getEpisodeTitle());
getSupportActionBar().setTitle(media.getFeedTitle());
return true;
}
return false;
}
@Override
@ -347,7 +340,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
Log.d(TAG, "ReloadNotification received, switching to Castplayer now");
destroyingDueToReload = true;
finish();
startActivity(new Intent(this, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true));
new MainActivityStarter(this).withOpenPlayer().start();
}
}

View File

@ -2,33 +2,34 @@ package de.danoeh.antennapod.activity;
import android.Manifest;
import android.app.WallpaperManager;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
public class WidgetConfigActivity extends AppCompatActivity {
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
private SeekBar opacitySeekBar;
private TextView opacityTextView;
private RelativeLayout widgetPreview;
private View widgetPreview;
private CheckBox ckRewind;
private CheckBox ckFastForward;
private CheckBox ckSkip;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -73,6 +74,32 @@ public class WidgetConfigActivity extends AppCompatActivity {
}
});
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);
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 = 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.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 displayDeviceBackground() {
@ -91,13 +118,16 @@ public class WidgetConfigActivity extends AppCompatActivity {
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_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();
PlayerWidgetJobService.updateWidget(this);
WidgetUpdaterJobService.performBackgroundUpdate(this);
}
private int getColorWithAlpha(int color, int opacity) {

View File

@ -1,395 +0,0 @@
package de.danoeh.antennapod.activity.gpoddernet;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.ViewFlipper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
/**
* Guides the user through the authentication process
* Step 1: Request username and password from user
* Step 2: Choose device from a list of available devices or create a new one
* Step 3: Choose from a list of actions
*/
public class GpodnetAuthenticationActivity extends AppCompatActivity {
private static final String TAG = "GpodnetAuthActivity";
private ViewFlipper viewFlipper;
private static final int STEP_DEFAULT = -1;
private static final int STEP_LOGIN = 0;
private static final int STEP_DEVICE = 1;
private static final int STEP_FINISH = 2;
private int currentStep = -1;
private GpodnetService service;
private volatile String username;
private volatile String password;
private volatile GpodnetDevice selectedDevice;
private View[] views;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.gpodnetauth_activity);
service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname());
viewFlipper = findViewById(R.id.viewflipper);
LayoutInflater inflater = (LayoutInflater)
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
views = new View[]{
inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false),
inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false),
inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false)
};
for (View view : views) {
viewFlipper.addView(view);
}
advance();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void setupLoginView(View view) {
final EditText username = view.findViewById(R.id.etxtUsername);
final EditText password = view.findViewById(R.id.etxtPassword);
final Button login = view.findViewById(R.id.butLogin);
final TextView txtvError = view.findViewById(R.id.txtvError);
final ProgressBar progressBar = view.findViewById(R.id.progBarLogin);
password.setOnEditorActionListener((v, actionID, event) ->
actionID == EditorInfo.IME_ACTION_GO && login.performClick());
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final String usernameStr = username.getText().toString();
final String passwordStr = password.getText().toString();
if (usernameHasUnwantedChars(usernameStr)) {
txtvError.setText(R.string.gpodnetsync_username_characters_error);
txtvError.setVisibility(View.VISIBLE);
return;
}
if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials");
AsyncTask<GpodnetService, Void, Void> authTask = new AsyncTask<GpodnetService, Void, Void>() {
volatile Exception exception;
@Override
protected void onPreExecute() {
super.onPreExecute();
login.setEnabled(false);
progressBar.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
// hide the keyboard
InputMethodManager inputManager = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(login.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
login.setEnabled(true);
progressBar.setVisibility(View.GONE);
if (exception == null) {
advance();
} else {
txtvError.setText(exception.getCause().getMessage());
txtvError.setVisibility(View.VISIBLE);
}
}
@Override
protected Void doInBackground(GpodnetService... params) {
try {
params[0].authenticate(usernameStr, passwordStr);
GpodnetAuthenticationActivity.this.username = usernameStr;
GpodnetAuthenticationActivity.this.password = passwordStr;
} catch (GpodnetServiceException e) {
e.printStackTrace();
exception = e;
}
return null;
}
};
authTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, service);
}
});
}
private void setupDeviceView(View view) {
final EditText deviceID = view.findViewById(R.id.etxtDeviceID);
final EditText caption = view.findViewById(R.id.etxtCaption);
final Button createNewDevice = view.findViewById(R.id.butCreateNewDevice);
final Button chooseDevice = view.findViewById(R.id.butChooseExistingDevice);
final TextView txtvError = view.findViewById(R.id.txtvError);
final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice);
final Spinner spinnerDevices = view.findViewById(R.id.spinnerChooseDevice);
// load device list
final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<>();
new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
chooseDevice.setEnabled(false);
spinnerDevices.setEnabled(false);
createNewDevice.setEnabled(false);
}
@Override
protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) {
super.onPostExecute(gpodnetDevices);
if (gpodnetDevices != null) {
List<String> deviceNames = new ArrayList<>();
for (GpodnetDevice device : gpodnetDevices) {
deviceNames.add(device.getCaption());
}
spinnerDevices.setAdapter(new ArrayAdapter<>(GpodnetAuthenticationActivity.this,
android.R.layout.simple_spinner_dropdown_item, deviceNames));
spinnerDevices.setEnabled(true);
if (!deviceNames.isEmpty()) {
chooseDevice.setEnabled(true);
}
devices.set(gpodnetDevices);
deviceID.setText(generateDeviceID(gpodnetDevices));
createNewDevice.setEnabled(true);
}
}
@Override
protected List<GpodnetDevice> doInBackground(GpodnetService... params) {
try {
return params[0].getDevices();
} catch (GpodnetServiceException e) {
e.printStackTrace();
return null;
}
}
}.execute(service);
createNewDevice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (checkDeviceIDText(deviceID, caption, txtvError, devices.get())) {
final String deviceStr = deviceID.getText().toString();
final String captionStr = caption.getText().toString();
new AsyncTask<GpodnetService, Void, GpodnetDevice>() {
private volatile Exception exception;
@Override
protected void onPreExecute() {
super.onPreExecute();
createNewDevice.setEnabled(false);
chooseDevice.setEnabled(false);
progBarCreateDevice.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
}
@Override
protected void onPostExecute(GpodnetDevice result) {
super.onPostExecute(result);
createNewDevice.setEnabled(true);
chooseDevice.setEnabled(true);
progBarCreateDevice.setVisibility(View.GONE);
if (exception == null) {
selectedDevice = result;
advance();
} else {
txtvError.setText(exception.getMessage());
txtvError.setVisibility(View.VISIBLE);
}
}
@Override
protected GpodnetDevice doInBackground(GpodnetService... params) {
try {
params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE);
return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
} catch (GpodnetServiceException e) {
e.printStackTrace();
exception = e;
}
return null;
}
}.execute(service);
}
}
});
chooseDevice.setOnClickListener(v -> {
final int position = spinnerDevices.getSelectedItemPosition();
if (position != AdapterView.INVALID_POSITION) {
selectedDevice = devices.get().get(position);
advance();
}
});
}
private String generateDeviceID(List<GpodnetDevice> gpodnetDevices) {
// devices names must be of a certain form:
// https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices
// This is more restrictive than needed, but I think it makes for more readable names.
String baseId = Build.MODEL.replaceAll("\\W", "");
String id = baseId;
int num = 0;
while (isDeviceWithIdInList(id, gpodnetDevices)) {
id = baseId + "_" + num;
num++;
}
return id;
}
private boolean isDeviceWithIdInList(String id, List<GpodnetDevice> gpodnetDevices) {
if (gpodnetDevices == null) {
return false;
}
for (GpodnetDevice device : gpodnetDevices) {
if (device.getId().equals(id)) {
return true;
}
}
return false;
}
private boolean checkDeviceIDText(EditText deviceID, EditText caption, TextView txtvError, List<GpodnetDevice> devices) {
String text = deviceID.getText().toString();
if (text.length() == 0) {
txtvError.setText(R.string.gpodnetauth_device_errorEmpty);
txtvError.setVisibility(View.VISIBLE);
return false;
} else if (caption.length() == 0) {
txtvError.setText(R.string.gpodnetauth_device_caption_errorEmpty);
txtvError.setVisibility(View.VISIBLE);
return false;
} else {
if (devices != null) {
if (isDeviceWithIdInList(text, devices)) {
txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed);
txtvError.setVisibility(View.VISIBLE);
return false;
}
txtvError.setVisibility(View.GONE);
return true;
}
return true;
}
}
private void setupFinishView(View view) {
final Button sync = view.findViewById(R.id.butSyncNow);
final Button back = view.findViewById(R.id.butGoMainscreen);
sync.setOnClickListener(v -> {
finish();
SyncService.sync(getApplicationContext());
});
back.setOnClickListener(v -> {
Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
});
}
private void writeLoginCredentials() {
if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials");
GpodnetPreferences.setUsername(username);
GpodnetPreferences.setPassword(password);
GpodnetPreferences.setDeviceID(selectedDevice.getId());
}
private void advance() {
if (currentStep < STEP_FINISH) {
View view = views[currentStep + 1];
if (currentStep == STEP_DEFAULT) {
setupLoginView(view);
} else if (currentStep == STEP_LOGIN) {
if (username == null || password == null) {
throw new IllegalStateException("Username and password must not be null here");
} else {
setupDeviceView(view);
}
} else if (currentStep == STEP_DEVICE) {
if (selectedDevice == null) {
throw new IllegalStateException("Device must not be null here");
} else {
writeLoginCredentials();
setupFinishView(view);
}
}
if (currentStep != STEP_DEFAULT) {
viewFlipper.showNext();
}
currentStep++;
} else {
finish();
}
}
private boolean usernameHasUnwantedChars(String username) {
Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]");
Matcher containsUnwantedChars = special.matcher(username);
return containsUnwantedChars.find();
}
}

View File

@ -20,9 +20,9 @@ import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.view.CircularProgressBar;
import de.danoeh.antennapod.ui.common.CircularProgressBar;
public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder> {
private Playable media;
@ -42,7 +42,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte
hasImages = false;
if (media.getChapters() != null) {
for (Chapter chapter : media.getChapters()) {
if (!ignoreChapter(chapter) && !TextUtils.isEmpty(chapter.getImageUrl())) {
if (!TextUtils.isEmpty(chapter.getImageUrl())) {
hasImages = true;
}
}
@ -125,14 +125,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte
if (media == null || media.getChapters() == null) {
return 0;
}
// ignore invalid chapters
int counter = 0;
for (Chapter chapter : media.getChapters()) {
if (!ignoreChapter(chapter)) {
counter++;
}
}
return counter;
return media.getChapters().size();
}
static class ChapterHolder extends RecyclerView.ViewHolder {
@ -171,22 +164,8 @@ public class ChaptersListAdapter extends RecyclerView.Adapter<ChaptersListAdapte
notifyItemChanged(currentChapterIndex, "foo");
}
private boolean ignoreChapter(Chapter c) {
return media.getDuration() > 0 && media.getDuration() < c.getStart();
}
public Chapter getItem(int position) {
int i = 0;
for (Chapter chapter : media.getChapters()) {
if (!ignoreChapter(chapter)) {
if (i == position) {
return chapter;
} else {
i++;
}
}
}
return null;
return media.getChapters().get(position);
}
public interface Callback {

View File

@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder;
/**
@ -68,16 +68,14 @@ public class DownloadLogAdapter extends BaseAdapter {
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 {
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));
String reasonText = status.getReason().getErrorString(context);
if (status.getReasonDetailed() != null) {
reasonText += ": " + status.getReasonDetailed();
}
holder.reason.setText(reasonText);
holder.reason.setText(status.getReason().getErrorString(context));
holder.reason.setVisibility(View.VISIBLE);
holder.tapForDetails.setVisibility(View.VISIBLE);
if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) {
holder.secondaryActionButton.setVisibility(View.INVISIBLE);

View File

@ -15,8 +15,8 @@ import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.view.CircularProgressBar;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.ui.common.CircularProgressBar;
public class DownloadlistAdapter extends BaseAdapter {
@ -68,8 +68,8 @@ public class DownloadlistAdapter extends BaseAdapter {
holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label));
holder.secondaryActionButton.setTag(downloader);
holder.secondaryActionButton.setOnClickListener(butSecondaryListener);
holder.secondaryActionProgress.setPercentage(0, request);
boolean percentageWasSet = false;
String status = "";
if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) {
status += context.getString(R.string.download_type_feed);
@ -85,8 +85,12 @@ public class DownloadlistAdapter extends BaseAdapter {
status += " / " + Formatter.formatShortFileSize(context, request.getSize());
holder.secondaryActionProgress.setPercentage(
0.01f * Math.max(1, request.getProgressPercent()), request);
percentageWasSet = true;
}
}
if (!percentageWasSet) {
holder.secondaryActionProgress.setPercentage(0, request);
}
holder.status.setText(status);
return convertView;

View File

@ -20,6 +20,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.dialog.StreamingConfirmationDialog;
import java.util.List;
@ -59,7 +60,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
holder.title.setText(item.getTitle());
holder.pubDate.setText(DateUtils.formatAbbrev(getContext(), item.getPubDate()));
if (item.getDescription() != null) {
String description = item.getDescription()
String description = HtmlToPlainText.getPlainText(item.getDescription())
.replaceAll("\n", " ")
.replaceAll("\\s+", " ")
.trim();

View File

@ -10,7 +10,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.view.SquareImageView;
import de.danoeh.antennapod.ui.common.SquareImageView;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

View File

@ -24,7 +24,6 @@ import de.danoeh.antennapod.core.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.core.util.ThemeUtils;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.DownloadsFragment;
import de.danoeh.antennapod.fragment.EpisodesFragment;
@ -32,6 +31,7 @@ import de.danoeh.antennapod.fragment.NavDrawerFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
@ -76,7 +76,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)) {
if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) {
loadItems();
}
}
@ -314,7 +314,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
}
Glide.with(context)
.load(feed.getImageLocation())
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)

View File

@ -66,7 +66,7 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter<Recycle
StatisticsHolder holder = (StatisticsHolder) h;
StatisticsItem statsItem = statisticsData.get(position - 1);
Glide.with(context)
.load(statsItem.feed.getImageLocation())
.load(statsItem.feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)

View File

@ -22,8 +22,8 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import jp.shts.android.library.TriangleLabelView;
/**
@ -112,7 +112,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
boolean textAndImageCombined = feed.isLocalFeed()
&& LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageLocation())
.withUri(feed.getImageUrl())
.withPlaceholderView(holder.feedTitle, textAndImageCombined)
.withCoverView(holder.imageView)
.load();
@ -123,7 +123,6 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
.withCoverView(holder.imageView)
.load();
}
return convertView;
}

View File

@ -2,21 +2,19 @@ package de.danoeh.antennapod.config;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.storage.APDownloadAlgorithm;
/**
* Configures the ClientConfig class of the core package.
*/
class ClientConfigurator {
private ClientConfigurator(){}
private ClientConfigurator() {
}
static {
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl();
ClientConfig.automaticDownloadAlgorithm = new APDownloadAlgorithm();
ClientConfig.castCallbacks = new CastCallbackImpl();
}
}

View File

@ -30,8 +30,8 @@ public class DownloadServiceCallbacksImpl implements DownloadServiceCallbacks {
@Override
public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request) {
final Intent activityIntent = new Intent(context.getApplicationContext(), DownloadAuthenticationActivity.class);
activityIntent.setAction("request" + request.getFeedfileId());
activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, request);
activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true);
return PendingIntent.getActivity(context.getApplicationContext(),
R.id.pending_intent_download_service_auth, activityIntent, PendingIntent.FLAG_ONE_SHOT);
}

View File

@ -1,24 +0,0 @@
package de.danoeh.antennapod.config;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.VideoplayerActivity;
import de.danoeh.antennapod.core.PlaybackServiceCallbacks;
import de.danoeh.antennapod.core.feed.MediaType;
public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks {
@Override
public Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback) {
if (mediaType == MediaType.AUDIO || remotePlayback) {
return new Intent(context, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true);
} else {
Intent i = new Intent(context, VideoplayerActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
}
return i;
}
}
}

View File

@ -1,37 +1,50 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.view.View;
import android.widget.EditText;
import android.text.method.HideReturnsTransformationMethod;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.databinding.AuthenticationDialogBinding;
/**
* Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
*/
public abstract class AuthenticationDialog extends AlertDialog.Builder {
boolean passwordHidden = true;
public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField,
String usernameInitialValue, String passwordInitialValue) {
super(context);
setTitle(titleRes);
View rootView = View.inflate(context, R.layout.authentication_dialog, null);
setView(rootView);
AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context));
setView(viewBinding.getRoot());
final EditText etxtUsername = rootView.findViewById(R.id.etxtUsername);
final EditText etxtPassword = rootView.findViewById(R.id.etxtPassword);
etxtUsername.setEnabled(enableUsernameField);
viewBinding.usernameEditText.setEnabled(enableUsernameField);
if (usernameInitialValue != null) {
etxtUsername.setText(usernameInitialValue);
viewBinding.usernameEditText.setText(usernameInitialValue);
}
if (passwordInitialValue != null) {
etxtPassword.setText(passwordInitialValue);
viewBinding.passwordEditText.setText(passwordInitialValue);
}
viewBinding.showPasswordButton.setOnClickListener(v -> {
if (passwordHidden) {
viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
viewBinding.showPasswordButton.setAlpha(1.0f);
} else {
viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
viewBinding.showPasswordButton.setAlpha(0.6f);
}
passwordHidden = !passwordHidden;
});
setOnCancelListener(dialog -> onCancelled());
setOnDismissListener(dialog -> onCancelled());
setNegativeButton(R.string.cancel_label, null);
setPositiveButton(R.string.confirm_label, (dialog, which)
-> onConfirmed(etxtUsername.getText().toString(), etxtPassword.getText().toString()));
-> onConfirmed(viewBinding.usernameEditText.getText().toString(),
viewBinding.passwordEditText.getText().toString()));
}
protected void onCancelled() {

View File

@ -28,7 +28,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.SortOrder;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import java.util.ArrayList;
import java.util.Arrays;

View File

@ -16,7 +16,7 @@ import java.util.Set;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.feed.FeedItemFilterGroup;
import de.danoeh.antennapod.view.RecursiveRadioGroup;
import de.danoeh.antennapod.ui.common.RecursiveRadioGroup;
public abstract class FilterDialog {

View File

@ -1,59 +0,0 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import android.text.Editable;
import android.text.InputType;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
/**
* Creates a dialog that lets the user change the hostname for the gpodder.net service.
*/
public class GpodnetSetHostnameDialog {
private GpodnetSetHostnameDialog(){}
private static final String TAG = "GpodnetSetHostnameDialog";
public static AlertDialog createDialog(final Context context) {
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
final EditText et = new EditText(context);
et.setText(GpodnetPreferences.getHostname());
et.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
dialog.setTitle(R.string.pref_gpodnet_sethostname_title)
.setView(setupContentView(context, et))
.setPositiveButton(R.string.confirm_label, (dialog1, which) -> {
final Editable e = et.getText();
if (e != null) {
GpodnetPreferences.setHostname(e.toString());
}
dialog1.dismiss();
})
.setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel())
.setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, (dialog1, which) -> {
GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST);
dialog1.dismiss();
})
.setCancelable(true);
return dialog.show();
}
private static View setupContentView(Context context, EditText et) {
LinearLayout ll = new LinearLayout(context);
ll.addView(et);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams();
if (params != null) {
params.setMargins(8, 8, 8, 8);
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
}
return ll;
}
}

View File

@ -41,7 +41,7 @@ public class PlaybackControlsDialog extends DialogFragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public void setupGUI() {
public void loadMediaInfo() {
setupUi();
setupAudioTracks();
}

View File

@ -47,12 +47,12 @@ public class SleepTimerDialog extends DialogFragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public void setupGUI() {
public void onSleepTimerUpdate() {
updateTime();
}
@Override
public void onSleepTimerUpdate() {
public void loadMediaInfo() {
updateTime();
}
};

View File

@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.view.RecursiveRadioGroup;
import de.danoeh.antennapod.ui.common.RecursiveRadioGroup;
public class SubscriptionsFilterDialog {
public static void showDialog(Context context) {

View File

@ -56,12 +56,12 @@ public class VariableSpeedDialog extends DialogFragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public void setupGUI() {
public void onPlaybackSpeedChange() {
updateSpeed();
}
@Override
public void onPlaybackSpeedChange() {
public void loadMediaInfo() {
updateSpeed();
}
};

View File

@ -18,7 +18,7 @@ public class GpodnetPodcastSearcher implements PodcastSearcher {
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
try {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
GpodnetPreferences.getHostname());
GpodnetPreferences.getHosturl());
List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0);
List<PodcastSearchResult> results = new ArrayList<>();
for (GpodnetPodcast podcast : gpodnetPodcasts) {

View File

@ -15,11 +15,11 @@ public class PodcastSearcherRegistry {
public static List<SearcherInfo> getSearchProviders() {
if (searchProviders == null) {
searchProviders = new ArrayList<>();
searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.f));
searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.f));
searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.f));
searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f));
searchProviders.add(new SearcherInfo(new GpodnetPodcastSearcher(), 0.0f));
searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 0.0f));
searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.0f));
searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.0f));
searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 1.0f));
}
return searchProviders;
}

View File

@ -50,9 +50,11 @@ public class AddFeedFragment extends Fragment {
public static final String TAG = "AddFeedFragment";
private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1;
private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2;
private static final String KEY_UP_ARROW = "up_arrow";
private AddfeedBinding viewBinding;
private MainActivity activity;
private boolean displayUpArrow;
@Override
@Nullable
@ -64,7 +66,11 @@ public class AddFeedFragment extends Fragment {
activity = (MainActivity) getActivity();
Toolbar toolbar = viewBinding.toolbar;
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
viewBinding.searchItunesButton.setOnClickListener(v
-> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class)));
@ -119,6 +125,12 @@ public class AddFeedFragment extends Fragment {
return viewBinding.getRoot();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
private void showAddViaUrlDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.add_podcast_by_url);
@ -196,7 +208,11 @@ public class AddFeedFragment extends Fragment {
if (documentFile == null) {
throw new IllegalArgumentException("Unable to retrieve document tree");
}
Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, documentFile.getName());
String title = documentFile.getName();
if (title == null) {
title = getString(R.string.local_folder);
}
Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title);
dirFeed.setItems(Collections.emptyList());
dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z);
Feed fromDatabase = DBTasks.updateFeed(getContext(), dirFeed, false);

View File

@ -104,13 +104,12 @@ public class AllEpisodesFragment extends EpisodesListFragment {
@NonNull
@Override
protected List<FeedItem> loadData() {
return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE));
return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter);
}
@NonNull
@Override
protected List<FeedItem> loadMoreData() {
return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE,
EPISODES_PER_PAGE));
return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter);
}
}

View File

@ -1,8 +1,6 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@ -17,7 +15,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
@ -29,11 +29,14 @@ import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.event.FavoritesEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
@ -45,7 +48,8 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView;
import de.danoeh.antennapod.view.ChapterSeekBar;
import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -62,14 +66,13 @@ import java.util.List;
* Shows the audio player.
*/
public class AudioPlayerFragment extends Fragment implements
SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
public static final String TAG = "AudioPlayerFragment";
private static final int POS_COVER = 0;
private static final int POS_DESCR = 1;
private static final int POS_CHAPTERS = 2;
private static final int NUM_CONTENT_FRAGMENTS = 3;
private static final String PREFS = "AudioPlayerFragmentPreferences";
private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
public static final String PREFS = "AudioPlayerFragmentPreferences";
private static final float EPSILON = 0.001f;
PlaybackSpeedIndicatorView butPlaybackSpeed;
@ -77,7 +80,7 @@ public class AudioPlayerFragment extends Fragment implements
private ViewPager2 pager;
private TextView txtvPosition;
private TextView txtvLength;
private SeekBar sbPosition;
private ChapterSeekBar sbPosition;
private ImageButton butRev;
private TextView txtvRev;
private ImageButton butPlay;
@ -86,6 +89,8 @@ public class AudioPlayerFragment extends Fragment implements
private ImageButton butSkip;
private Toolbar toolbar;
private ProgressBar progressIndicator;
private CardView cardViewSeek;
private TextView txtvSeek;
private PlaybackController controller;
private Disposable disposable;
@ -122,6 +127,8 @@ public class AudioPlayerFragment extends Fragment implements
txtvFF = root.findViewById(R.id.txtvFF);
butSkip = root.findViewById(R.id.butSkip);
progressIndicator = root.findViewById(R.id.progLoading);
cardViewSeek = root.findViewById(R.id.cardViewSeek);
txtvSeek = root.findViewById(R.id.txtvSeek);
setupLengthTextView();
setupControlButtons();
@ -168,12 +175,33 @@ public class AudioPlayerFragment extends Fragment implements
return root;
}
public void setHasChapters(boolean hasChapters) {
private void setHasChapters(boolean hasChapters) {
this.hasChapters = hasChapters;
tabLayoutMediator.detach();
tabLayoutMediator.attach();
}
private void setChapterDividers(Playable media) {
if (media == null) {
return;
}
float[] dividerPos = null;
if (hasChapters) {
List<Chapter> chapters = media.getChapters();
dividerPos = new float[chapters.size()];
float duration = media.getDuration();
for (int i = 0; i < chapters.size(); i++) {
dividerPos[i] = chapters.get(i).getStart() / duration;
}
}
sbPosition.setDividerPos(dividerPos);
}
public View getExternalPlayerHolder() {
return getView().findViewById(R.id.playerFragment);
}
@ -211,16 +239,25 @@ public class AudioPlayerFragment extends Fragment implements
IntentUtils.sendLocalBroadcast(getActivity(), PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) {
if (controller == null) {
return;
}
updatePosition(new PlaybackPositionEvent(controller.getPosition(),
controller.getDuration()));
}
private void setupLengthTextView() {
SharedPreferences prefs = getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
showTimeLeft = UserPreferences.shouldShowRemainingTime();
txtvLength.setOnClickListener(v -> {
if (controller == null) {
return;
}
showTimeLeft = !showTimeLeft;
prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft).apply();
updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration()));
UserPreferences.setShowRemainTimeSetting(showTimeLeft);
updatePosition(new PlaybackPositionEvent(controller.getPosition(),
controller.getDuration()));
});
}
@ -285,26 +322,21 @@ public class AudioPlayerFragment extends Fragment implements
disposable = Maybe.create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
ChapterUtils.loadChapters(media, getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> updateUi((Playable) media),
error -> Log.e(TAG, Log.getStackTraceString(error)),
() -> updateUi(null));
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> updateUi((Playable) media),
error -> Log.e(TAG, Log.getStackTraceString(error)),
() -> updateUi(null));
}
private PlaybackController newPlaybackController() {
return new PlaybackController(getActivity()) {
@Override
public void setupGUI() {
AudioPlayerFragment.this.loadMediaInfo();
}
@Override
public void onBufferStart() {
progressIndicator.setVisibility(View.VISIBLE);
@ -352,9 +384,8 @@ public class AudioPlayerFragment extends Fragment implements
}
@Override
public boolean loadMediaInfo() {
public void loadMediaInfo() {
AudioPlayerFragment.this.loadMediaInfo();
return true;
}
@Override
@ -383,8 +414,15 @@ public class AudioPlayerFragment extends Fragment implements
if (controller == null) {
return;
}
if (media != null && media.getChapters() != null) {
setHasChapters(media.getChapters().size() > 0);
} else {
setHasChapters(false);
}
updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration()));
updatePlaybackSpeedButton(media);
setChapterDividers(media);
setupOptionsMenu(media);
}
@ -433,6 +471,7 @@ public class AudioPlayerFragment extends Fragment implements
return;
}
txtvPosition.setText(Converter.getDurationStringLong(currentPosition));
showTimeLeft = UserPreferences.shouldShowRemainingTime();
if (showTimeLeft) {
txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime));
} else {
@ -454,22 +493,22 @@ public class AudioPlayerFragment extends Fragment implements
}
if (fromUser) {
float prog = progress / ((float) seekBar.getMax());
int duration = controller.getDuration();
TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier());
int position = converter.convert((int) (prog * duration));
txtvPosition.setText(Converter.getDurationStringLong(position));
if (showTimeLeft && prog != 0) {
int timeLeft = converter.convert(duration - (int) (prog * duration));
String length = "-" + Converter.getDurationStringLong(timeLeft);
txtvLength.setText(length);
}
int position = converter.convert((int) (prog * controller.getDuration()));
txtvSeek.setText(Converter.getDurationStringLong(position));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// interrupt position Observer, restart later
cardViewSeek.setScaleX(.8f);
cardViewSeek.setScaleY(.8f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start();
}
@Override
@ -478,6 +517,13 @@ public class AudioPlayerFragment extends Fragment implements
float prog = seekBar.getProgress() / ((float) seekBar.getMax());
controller.seekTo((int) (prog * controller.getDuration()));
}
cardViewSeek.setScaleX(1f);
cardViewSeek.setScaleY(1f);
cardViewSeek.animate()
.setInterpolator(new FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start();
}
public void setupOptionsMenu(Playable media) {

View File

@ -8,9 +8,9 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
@ -45,7 +45,8 @@ public class ChaptersFragment extends Fragment {
RecyclerView recyclerView = root.findViewById(R.id.recyclerView);
layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(),
layoutManager.getOrientation()));
adapter = new ChaptersListAdapter(getActivity(), pos -> {
if (controller.getStatus() != PlayerStatus.PLAYING) {
@ -71,13 +72,7 @@ public class ChaptersFragment extends Fragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public boolean loadMediaInfo() {
ChaptersFragment.this.loadMediaInfo();
return true;
}
@Override
public void setupGUI() {
public void loadMediaInfo() {
ChaptersFragment.this.loadMediaInfo();
}
@ -123,7 +118,7 @@ public class ChaptersFragment extends Fragment {
disposable = Maybe.create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
media.loadChapterMarks(getContext());
ChapterUtils.loadChapters(media, getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
@ -142,7 +137,6 @@ public class ChaptersFragment extends Fragment {
return;
}
adapter.setMedia(media);
((AudioPlayerFragment) getParentFragment()).setHasChapters(adapter.getItemCount() > 0);
int positionOfCurrentChapter = getCurrentChapter(media);
updateChapterSelection(positionOfCurrentChapter);
}

View File

@ -13,11 +13,9 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
@ -25,9 +23,11 @@ import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
@ -35,6 +35,7 @@ import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@ -93,7 +94,12 @@ public class CoverFragment extends Fragment {
}
private void displayMediaInfo(@NonNull Playable media) {
txtvPodcastTitle.setText(media.getFeedTitle());
String pubDateStr = DateUtils.formatAbbrev(getActivity(), ((FeedMedia) media).getPubDate());
txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle())
+ "\u00A0"
+ ""
+ "\u00A0"
+ StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0"));
txtvEpisodeTitle.setText(media.getEpisodeTitle());
displayedChapterIndex = -2; // Force refresh
displayCoverImage(media.getPosition());
@ -111,13 +117,7 @@ public class CoverFragment extends Fragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public boolean loadMediaInfo() {
CoverFragment.this.loadMediaInfo();
return true;
}
@Override
public void setupGUI() {
public void loadMediaInfo() {
CoverFragment.this.loadMediaInfo();
}
};
@ -151,23 +151,25 @@ public class CoverFragment extends Fragment {
if (chapter != displayedChapterIndex) {
displayedChapterIndex = chapter;
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transforms(new FitCenter(),
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)));
RequestBuilder<Drawable> cover = Glide.with(this)
.load(ImageResourceUtils.getImageLocation(media))
.apply(new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transforms(new FitCenter(),
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))));
.load(media.getImageLocation())
.error(Glide.with(this)
.load(ImageResourceUtils.getFallbackImageLocation(media))
.apply(options))
.apply(options);
if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) {
cover.into(imgvCover);
} else {
Glide.with(this)
.load(EmbeddedChapterImage.getModelFor(media, chapter))
.apply(new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transforms(new FitCenter(),
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))))
.apply(options)
.thumbnail(cover)
.error(cover)
.into(imgvCover);
@ -208,7 +210,7 @@ public class CoverFragment extends Fragment {
imgvCover.setLayoutParams(params);
}
} else {
double percentageHeight = ratio * 0.8;
double percentageHeight = ratio * 0.6;
mainContainer.setOrientation(LinearLayout.HORIZONTAL);
if (newConfig.screenHeightDp > 0) {
params.height = (int) (convertDpToPixel(newConfig.screenHeightDp) * percentageHeight);

View File

@ -28,16 +28,17 @@ public class DownloadsFragment extends PagedToolbarFragment {
public static final String TAG = "DownloadsFragment";
public static final String ARG_SELECTED_TAB = "selected_tab";
private static final String PREF_LAST_TAB_POSITION = "tab_position";
private static final String KEY_UP_ARROW = "up_arrow";
public static final int POS_RUNNING = 0;
private static final int POS_COMPLETED = 1;
public static final int POS_LOG = 2;
private static final int TOTAL_COUNT = 3;
private static final String PREF_LAST_TAB_POSITION = "tab_position";
private ViewPager2 viewPager;
private TabLayout tabLayout;
private boolean displayUpArrow;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@ -48,7 +49,11 @@ public class DownloadsFragment extends PagedToolbarFragment {
Toolbar toolbar = root.findViewById(R.id.toolbar);
toolbar.setTitle(R.string.downloads_label);
toolbar.inflateMenu(R.menu.downloads);
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
viewPager = root.findViewById(R.id.viewpager);
viewPager.setAdapter(new DownloadsPagerAdapter(this));
@ -81,6 +86,12 @@ public class DownloadsFragment extends PagedToolbarFragment {
return root;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

View File

@ -24,6 +24,7 @@ public class EpisodesFragment extends PagedToolbarFragment {
public static final String TAG = "EpisodesFragment";
private static final String PREF_LAST_TAB_POSITION = "tab_position";
private static final String KEY_UP_ARROW = "up_arrow";
private static final int POS_NEW_EPISODES = 0;
private static final int POS_ALL_EPISODES = 1;
@ -31,6 +32,7 @@ public class EpisodesFragment extends PagedToolbarFragment {
private static final int TOTAL_COUNT = 3;
private TabLayout tabLayout;
private boolean displayUpArrow;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -44,7 +46,11 @@ public class EpisodesFragment extends PagedToolbarFragment {
toolbar.setTitle(R.string.episodes_label);
toolbar.inflateMenu(R.menu.episodes);
MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, "");
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
ViewPager2 viewPager = rootView.findViewById(R.id.viewpager);
viewPager.setAdapter(new EpisodesPagerAdapter(this));
@ -88,6 +94,12 @@ public class EpisodesFragment extends PagedToolbarFragment {
editor.apply();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
static class EpisodesPagerAdapter extends FragmentStateAdapter {
EpisodesPagerAdapter(@NonNull Fragment fragment) {

View File

@ -383,6 +383,14 @@ public abstract class EpisodesListFragment extends Fragment {
@NonNull
protected abstract List<FeedItem> loadData();
/**
* Load a new page of data as defined by {@link #page} and {@link #EPISODES_PER_PAGE}.
* If the number of items returned is less than {@link #EPISODES_PER_PAGE},
* it will be assumed that the underlying data is exhausted
* and this method will not be called again.
*
* @return The items from the next page of data
*/
@NonNull
protected abstract List<FeedItem> loadMoreData();
}

View File

@ -108,12 +108,7 @@ public class ExternalPlayerFragment extends Fragment {
}
@Override
public boolean loadMediaInfo() {
return ExternalPlayerFragment.this.loadMediaInfo();
}
@Override
public void setupGUI() {
public void loadMediaInfo() {
ExternalPlayerFragment.this.loadMediaInfo();
}
@ -170,11 +165,11 @@ public class ExternalPlayerFragment extends Fragment {
}
}
private boolean loadMediaInfo() {
private void loadMediaInfo() {
Log.d(TAG, "Loading media info");
if (controller == null) {
Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!");
return false;
return;
}
if (disposable != null) {
@ -186,7 +181,6 @@ public class ExternalPlayerFragment extends Fragment {
.subscribe(this::updateUi,
error -> Log.e(TAG, Log.getStackTraceString(error)),
() -> ((MainActivity) getActivity()).setPlayerVisible(false));
return true;
}
private void updateUi(Playable media) {
@ -198,14 +192,19 @@ public class ExternalPlayerFragment extends Fragment {
feedName.setText(media.getFeedTitle());
onPositionObserverUpdate();
RequestOptions options = new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate();
Glide.with(getActivity())
.load(ImageResourceUtils.getImageLocation(media))
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.load(ImageResourceUtils.getEpisodeListImageLocation(media))
.error(Glide.with(getActivity())
.load(ImageResourceUtils.getFallbackImageLocation(media))
.apply(options))
.apply(options)
.into(imgvCover);
if (controller != null && controller.isPlayingVideoLocally()) {

View File

@ -45,7 +45,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.StatisticsItem;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.fragment.preferences.StatisticsFragment;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
@ -130,6 +130,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
protected void doTint(Context themedContext) {
toolbar.getMenu().findItem(R.id.visit_website_item)
.setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.location_web_site));
toolbar.getMenu().findItem(R.id.share_parent)
.setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_share));
}
};
iconTintManager.updateTint();
@ -201,7 +203,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
Log.d(TAG, "Author is " + feed.getAuthor());
Log.d(TAG, "URL is " + feed.getDownload_url());
Glide.with(getContext())
.load(feed.getImageLocation())
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
@ -210,7 +212,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
.dontAnimate())
.into(imgvCover);
Glide.with(getContext())
.load(feed.getImageLocation())
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
@ -284,9 +286,13 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
}
private void refreshToolbarState() {
boolean shareLinkVisible = feed != null && feed.getLink() != null;
boolean downloadUrlVisible = feed != null && !feed.isLocalFeed();
toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed());
toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(feed != null && !feed.isLocalFeed());
toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null);
toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(downloadUrlVisible);
toolbar.getMenu().findItem(R.id.share_link_item).setVisible(shareLinkVisible);
toolbar.getMenu().findItem(R.id.share_parent).setVisible(downloadUrlVisible || shareLinkVisible);
toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null
&& IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink()))));
}

View File

@ -16,7 +16,6 @@ import android.widget.AdapterView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -57,8 +56,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.Optional;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.dialog.FilterDialog;
@ -89,6 +87,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
Toolbar.OnMenuItemClickListener {
private static final String TAG = "ItemlistFragment";
private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
private static final String KEY_UP_ARROW = "up_arrow";
private FeedItemListAdapter adapter;
private MoreContentListFooterUtil nextPageLoader;
@ -106,6 +105,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private View header;
private Toolbar toolbar;
private ToolbarIconTintManager iconTintManager;
private boolean displayUpArrow;
private long feedID;
private Feed feed;
@ -146,7 +146,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
toolbar = root.findViewById(R.id.toolbar);
toolbar.inflateMenu(R.menu.feedlist);
toolbar.setOnMenuItemClickListener(this);
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
refreshToolbarState();
recyclerView = root.findViewById(R.id.recyclerView);
@ -231,6 +235,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
adapter = null;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() {
@Override
public boolean isRefreshing() {
@ -451,10 +461,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (feed.getItemFilter() != null) {
FeedItemFilter filter = feed.getItemFilter();
if (filter.getValues().length > 0) {
if (feed.hasLastUpdateFailed()) {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) txtvInformation.getLayoutParams();
p.addRule(RelativeLayout.BELOW, R.id.txtvFailure);
}
txtvInformation.setText("{md-info-outline} " + this.getString(R.string.filtered_label));
Iconify.addIcons(txtvInformation);
txtvInformation.setOnClickListener((l) -> {
@ -514,7 +520,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private void loadFeedImage() {
Glide.with(getActivity())
.load(feed.getImageLocation())
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
@ -524,7 +530,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
.into(imgvBackground);
Glide.with(getActivity())
.load(feed.getImageLocation())
.load(feed.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
@ -542,27 +548,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
disposable = Observable.fromCallable(this::loadData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result.orElse(null);
refreshHeaderView();
displayList();
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
.subscribe(
result -> {
feed = result;
refreshHeaderView();
displayList();
}, error -> {
feed = null;
refreshHeaderView();
displayList();
Log.e(TAG, Log.getStackTraceString(error));
});
}
@NonNull
private Optional<Feed> loadData() {
Feed feed = DBReader.getFeed(feedID);
if (feed != null && feed.getItemFilter() != null) {
DBReader.loadAdditionalFeedItemListData(feed.getItems());
FeedItemFilter filter = feed.getItemFilter();
feed.setItems(filter.filter(feed.getItems()));
@Nullable
private Feed loadData() {
Feed feed = DBReader.getFeed(feedID, true);
if (feed == null) {
return null;
}
if (feed != null && feed.getSortOrder() != null) {
DBReader.loadAdditionalFeedItemListData(feed.getItems());
if (feed.getSortOrder() != null) {
List<FeedItem> feedItems = feed.getItems();
FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems);
feed.setItems(feedItems);
}
return Optional.ofNullable(feed);
return feed;
}
private static class FeedItemListAdapter extends EpisodeItemListAdapter {

View File

@ -164,6 +164,7 @@ public class FeedSettingsFragment extends Fragment {
setupEpisodeFilterPreference();
setupPlaybackSpeedPreference();
setupFeedAutoSkipPreference();
setupEpisodeNotificationPreference();
setupTags();
updateAutoDeleteSummary();
@ -198,7 +199,7 @@ public class FeedSettingsFragment extends Fragment {
protected void onConfirmed(int skipIntro, int skipEnding) {
feedPreferences.setFeedSkipIntro(skipIntro);
feedPreferences.setFeedSkipEnding(skipEnding);
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
EventBus.getDefault().post(
new SkipIntroEndingChangedEvent(feedPreferences.getFeedSkipIntro(),
feedPreferences.getFeedSkipEnding(),
@ -226,7 +227,7 @@ public class FeedSettingsFragment extends Fragment {
feedPlaybackSpeedPreference.setEntries(entries);
feedPlaybackSpeedPreference.setOnPreferenceChangeListener((preference, newValue) -> {
feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) newValue));
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
updatePlaybackSpeedPreference();
EventBus.getDefault().post(
new SpeedPresetChangedEvent(feedPreferences.getFeedPlaybackSpeed(), feed.getId()));
@ -240,7 +241,7 @@ public class FeedSettingsFragment extends Fragment {
@Override
protected void onConfirmed(FeedFilter filter) {
feedPreferences.setFilter(filter);
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
}
}.show();
return false;
@ -256,7 +257,7 @@ public class FeedSettingsFragment extends Fragment {
protected void onConfirmed(String username, String password) {
feedPreferences.setUsername(username);
feedPreferences.setPassword(password);
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
}
}.show();
return false;
@ -276,7 +277,7 @@ public class FeedSettingsFragment extends Fragment {
feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
break;
}
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
updateAutoDeleteSummary();
return false;
});
@ -322,7 +323,7 @@ public class FeedSettingsFragment extends Fragment {
feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION);
break;
}
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
updateVolumeReductionValue();
EventBus.getDefault().post(
new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId()));
@ -353,7 +354,7 @@ public class FeedSettingsFragment extends Fragment {
pref.setOnPreferenceChangeListener((preference, newValue) -> {
boolean checked = newValue == Boolean.TRUE;
feedPreferences.setKeepUpdated(checked);
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
pref.setChecked(checked);
return false;
});
@ -384,7 +385,7 @@ public class FeedSettingsFragment extends Fragment {
boolean checked = newValue == Boolean.TRUE;
feedPreferences.setAutoDownload(checked);
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
updateAutoDownloadEnabled();
ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(getActivity(), checked);
dialog.createNewDialog().show();
@ -412,7 +413,7 @@ public class FeedSettingsFragment extends Fragment {
feedPreferences.getTags().clear();
feedPreferences.getTags().addAll(new HashSet<>(Arrays.asList(
foldersString.split(FeedPreferences.TAG_SEPARATOR))));
feed.savePreferences();
DBWriter.setFeedPreferences(feedPreferences);
})
.setNegativeButton(R.string.cancel_label, null)
.show();
@ -420,6 +421,19 @@ public class FeedSettingsFragment extends Fragment {
});
}
private void setupEpisodeNotificationPreference() {
SwitchPreferenceCompat pref = findPreference("episodeNotification");
pref.setChecked(feedPreferences.getShowEpisodeNotification());
pref.setOnPreferenceChangeListener((preference, newValue) -> {
boolean checked = newValue == Boolean.TRUE;
feedPreferences.setShowEpisodeNotification(checked);
DBWriter.setFeedPreferences(feedPreferences);
pref.setChecked(checked);
return false;
});
}
private class ApplyToEpisodesDialog extends ConfirmationDialog {
private final boolean autoDownload;

View File

@ -10,6 +10,9 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.view.ShownotesWebView;
@ -82,7 +85,15 @@ public class ItemDescriptionFragment extends Fragment {
webViewLoader.dispose();
}
webViewLoader = Maybe.<String>create(emitter -> {
Timeline timeline = new Timeline(getActivity(), controller.getMedia());
Playable media = controller.getMedia();
if (media instanceof FeedMedia) {
FeedMedia feedMedia = ((FeedMedia) media);
if (feedMedia.getItem() == null) {
feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
}
DBReader.loadDescriptionOfFeedItem(feedMedia.getItem());
}
Timeline timeline = new Timeline(getActivity(), media.getDescription(), media.getDuration());
emitter.onSuccess(timeline.processShownotes());
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -140,14 +151,8 @@ public class ItemDescriptionFragment extends Fragment {
super.onStart();
controller = new PlaybackController(getActivity()) {
@Override
public boolean loadMediaInfo() {
public void loadMediaInfo() {
load();
return true;
}
@Override
public void setupGUI() {
ItemDescriptionFragment.this.load();
}
};
controller.init();

View File

@ -57,7 +57,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.view.ShownotesWebView;
@ -238,7 +238,12 @@ public class ItemFragment extends Fragment {
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
controller = new PlaybackController(getActivity());
controller = new PlaybackController(getActivity()) {
@Override
public void loadMediaInfo() {
// Do nothing
}
};
controller.init();
}
@ -291,14 +296,19 @@ public class ItemFragment extends Fragment {
txtvPublished.setContentDescription(DateUtils.formatForAccessibility(getContext(), item.getPubDate()));
}
RequestOptions options = new RequestOptions()
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transforms(new FitCenter(),
new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density)))
.dontAnimate();
Glide.with(getActivity())
.load(ImageResourceUtils.getImageLocation(item))
.apply(new RequestOptions()
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transforms(new FitCenter(),
new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density)))
.dontAnimate())
.load(item.getImageLocation())
.error(Glide.with(getActivity())
.load(ImageResourceUtils.getFallbackImageLocation(item))
.apply(options))
.apply(options)
.into(imgvCover);
updateButtons();
}
@ -429,7 +439,9 @@ public class ItemFragment extends Fragment {
FeedItem feedItem = DBReader.getFeedItem(itemId);
Context context = getContext();
if (feedItem != null && context != null) {
Timeline t = new Timeline(context, feedItem);
int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE;
DBReader.loadDescriptionOfFeedItem(feedItem);
Timeline t = new Timeline(context, feedItem.getDescription(), duration);
webviewData = t.processShownotes();
}
return feedItem;

View File

@ -424,7 +424,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
flatItemList = result.second;
updateSelection(); // Selected item might be a feed
navAdapter.notifyDataSetChanged();
progressBar.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
progressBar.setVisibility(View.GONE);

View File

@ -41,6 +41,7 @@ import java.util.List;
public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
public static final String TAG = "PlaybackHistoryFragment";
private static final String KEY_UP_ARROW = "up_arrow";
private List<FeedItem> playbackHistory;
private PlaybackHistoryListAdapter adapter;
@ -49,6 +50,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
private EmptyViewHandler emptyView;
private ProgressBar progressBar;
private Toolbar toolbar;
private boolean displayUpArrow;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -63,7 +65,11 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
toolbar = root.findViewById(R.id.toolbar);
toolbar.setTitle(R.string.playback_history_label);
toolbar.setOnMenuItemClickListener(this);
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
toolbar.inflateMenu(R.menu.playback_history);
refreshToolbarState();
@ -98,6 +104,12 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");

View File

@ -12,6 +12,7 @@ import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
@ -67,6 +68,7 @@ import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REM
*/
public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
public static final String TAG = "QueueFragment";
private static final String KEY_UP_ARROW = "up_arrow";
private TextView infoBar;
private EpisodeItemListRecyclerView recyclerView;
@ -74,6 +76,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
private EmptyViewHandler emptyView;
private ProgressBar progLoading;
private Toolbar toolbar;
private boolean displayUpArrow;
private List<FeedItem> queue;
@ -420,7 +423,11 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
View root = inflater.inflate(R.layout.queue_fragment, container, false);
toolbar = root.findViewById(R.id.toolbar);
toolbar.setOnMenuItemClickListener(this);
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
toolbar.inflateMenu(R.menu.queue);
MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, "");
refreshToolbarState();
@ -530,6 +537,12 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
return root;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
private void onFragmentLoaded(final boolean restoreScrollPosition) {
if (queue != null && queue.size() > 0) {
if (recyclerAdapter == null) {

View File

@ -8,6 +8,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
@ -67,6 +68,8 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
public static final String TAG = "SubscriptionFragment";
private static final String PREFS = "SubscriptionFragment";
private static final String PREF_NUM_COLUMNS = "columns";
private static final String KEY_UP_ARROW = "up_arrow";
private static final int MIN_NUM_COLUMNS = 2;
private static final int[] COLUMN_CHECKBOX_IDS = {
R.id.subscription_num_columns_2,
@ -85,6 +88,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
private int mPosition = -1;
private boolean isUpdatingFeeds = false;
private boolean displayUpArrow;
private Disposable disposable;
private SharedPreferences prefs;
@ -103,7 +107,11 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
View root = inflater.inflate(R.layout.fragment_subscriptions, container, false);
toolbar = root.findViewById(R.id.toolbar);
toolbar.setOnMenuItemClickListener(this);
((MainActivity) getActivity()).setupToolbarToggle(toolbar);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW);
}
((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow);
toolbar.inflateMenu(R.menu.subscriptions);
for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) {
// Do this in Java to localize numbers
@ -130,6 +138,12 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
return root;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(KEY_UP_ARROW, displayUpArrow);
super.onSaveInstanceState(outState);
}
private void refreshToolbarState() {
int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns());
toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true);
@ -218,16 +232,19 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
disposable.dispose();
}
emptyView.hide();
progressBar.setVisibility(View.VISIBLE);
disposable = Observable.fromCallable(DBReader::getNavDrawerData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
navDrawerData = result;
subscriptionAdapter.notifyDataSetChanged();
emptyView.updateVisibility();
progressBar.setVisibility(View.GONE);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
.subscribe(
result -> {
navDrawerData = result;
subscriptionAdapter.notifyDataSetChanged();
emptyView.updateVisibility();
progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
progressBar.setVisibility(View.GONE);
});
if (UserPreferences.getSubscriptionsFilter().isEnabled()) {
feedsFilteredMsg.setText("{md-info-outline} " + getString(R.string.subscriptions_are_filtered));

View File

@ -1,10 +1,7 @@
package de.danoeh.antennapod.fragment.gpodnet;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@ -13,9 +10,7 @@ import android.widget.Button;
import android.widget.GridView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
@ -25,6 +20,12 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
/**
* Displays a list of GPodnetPodcast-Objects in a GridView
@ -36,6 +37,7 @@ public abstract class PodcastListFragment extends Fragment {
private ProgressBar progressBar;
private TextView txtvError;
private Button butRetry;
private Disposable disposable;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -64,60 +66,44 @@ public abstract class PodcastListFragment extends Fragment {
protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException;
final void loadData() {
AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() {
volatile Exception exception = null;
@Override
protected List<GpodnetPodcast> doInBackground(Void... params) {
try {
if (disposable != null) {
disposable.dispose();
}
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
GpodnetPreferences.getHostname());
GpodnetPreferences.getHosturl());
return loadPodcastData(service);
} catch (GpodnetServiceException e) {
exception = e;
e.printStackTrace();
return null;
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
podcasts -> {
progressBar.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
@Override
protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) {
super.onPostExecute(gpodnetPodcasts);
final Context context = getActivity();
if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) {
PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts);
gridView.setAdapter(listAdapter);
listAdapter.notifyDataSetChanged();
progressBar.setVisibility(View.GONE);
gridView.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
} else if (context != null && gpodnetPodcasts != null) {
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
txtvError.setText(getString(R.string.search_status_no_results));
txtvError.setVisibility(View.VISIBLE);
butRetry.setVisibility(View.GONE);
} else if (context != null) {
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage());
txtvError.setVisibility(View.VISIBLE);
butRetry.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPreExecute() {
super.onPreExecute();
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
}
};
loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (podcasts.size() > 0) {
PodcastListAdapter listAdapter = new PodcastListAdapter(getContext(), 0, podcasts);
gridView.setAdapter(listAdapter);
listAdapter.notifyDataSetChanged();
gridView.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
} else {
gridView.setVisibility(View.GONE);
txtvError.setText(getString(R.string.search_status_no_results));
txtvError.setVisibility(View.VISIBLE);
}
}, error -> {
gridView.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
txtvError.setText(getString(R.string.error_msg_prefix) + error.getMessage());
txtvError.setVisibility(View.VISIBLE);
butRetry.setVisibility(View.VISIBLE);
Log.e(TAG, Log.getStackTraceString(error));
});
}
}

View File

@ -1,32 +1,34 @@
package de.danoeh.antennapod.fragment.gpodnet;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.ListFragment;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag;
import java.util.List;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class TagListFragment extends ListFragment {
private static final int COUNT = 50;
private static final String TAG = "TagListFragment";
private Disposable disposable;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getListView().setOnItemClickListener((parent, view1, position, id) -> {
GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position);
MainActivity activity = (MainActivity) getActivity();
activity.loadChildFragment(TagFragment.newInstance(tag));
((MainActivity) getActivity()).loadChildFragment(TagFragment.newInstance(tag));
});
startLoadTask();
@ -35,59 +37,36 @@ public class TagListFragment extends ListFragment {
@Override
public void onDestroyView() {
super.onDestroyView();
cancelLoadTask();
}
private AsyncTask<Void, Void, List<GpodnetTag>> loadTask;
private void cancelLoadTask() {
if (loadTask != null && !loadTask.isCancelled()) {
loadTask.cancel(true);
if (disposable != null) {
disposable.dispose();
}
}
private void startLoadTask() {
cancelLoadTask();
loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() {
private Exception exception;
@Override
protected List<GpodnetTag> doInBackground(Void... params) {
if (disposable != null) {
disposable.dispose();
}
setListShown(false);
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
GpodnetPreferences.getHostname());
try {
return service.getTopTags(COUNT);
} catch (GpodnetServiceException e) {
e.printStackTrace();
exception = e;
return null;
}
}
@Override
protected void onPreExecute() {
super.onPreExecute();
setListShown(false);
}
@Override
protected void onPostExecute(List<GpodnetTag> gpodnetTags) {
super.onPostExecute(gpodnetTags);
final Context context = getActivity();
if (context != null) {
if (gpodnetTags != null) {
setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags));
} else if (exception != null) {
GpodnetPreferences.getHosturl());
return service.getTopTags(COUNT);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
tags -> {
setListAdapter(new TagListAdapter(getContext(), android.R.layout.simple_list_item_1, tags));
setListShown(true);
}, error -> {
TextView txtvError = new TextView(getActivity());
txtvError.setText(exception.getMessage());
txtvError.setText(error.getMessage());
getListView().setEmptyView(txtvError);
}
setListShown(true);
}
}
};
loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
setListShown(true);
Log.e(TAG, Log.getStackTraceString(error));
});
}
}

View File

@ -174,7 +174,9 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat {
String[] entries = new String[values.length];
for (int x = 0; x < values.length; x++) {
int v = Integer.parseInt(values[x]);
if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) {
if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) {
entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal);
} else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) {
entries[x] = res.getString(R.string.episode_cleanup_queue_removal);
} else if (v == UserPreferences.EPISODE_CLEANUP_NULL){
entries[x] = res.getString(R.string.episode_cleanup_never);

View File

@ -0,0 +1,307 @@
package de.danoeh.antennapod.fragment.preferences;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Paint;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.ViewFlipper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputLayout;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.IntentUtils;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Guides the user through the authentication process.
*/
public class GpodderAuthenticationFragment extends DialogFragment {
public static final String TAG = "GpodnetAuthActivity";
private ViewFlipper viewFlipper;
private static final int STEP_DEFAULT = -1;
private static final int STEP_HOSTNAME = 0;
private static final int STEP_LOGIN = 1;
private static final int STEP_DEVICE = 2;
private static final int STEP_FINISH = 3;
private int currentStep = -1;
private GpodnetService service;
private volatile String username;
private volatile String password;
private volatile GpodnetDevice selectedDevice;
private List<GpodnetDevice> devices;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setTitle(GpodnetService.DEFAULT_BASE_HOST);
dialog.setNegativeButton(R.string.cancel_label, null);
dialog.setCancelable(false);
this.setCancelable(false);
View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null);
viewFlipper = root.findViewById(R.id.viewflipper);
advance();
dialog.setView(root);
return dialog.create();
}
private void setupHostView(View view) {
final Button selectHost = view.findViewById(R.id.chooseHostButton);
final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup);
final EditText serverUrlText = view.findViewById(R.id.serverUrlText);
if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHosturl())) {
serverUrlText.setText(GpodnetPreferences.getHosturl());
}
final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput);
serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE);
});
selectHost.setOnClickListener(v -> {
if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) {
GpodnetPreferences.setHosturl(serverUrlText.getText().toString());
} else {
GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST);
}
service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl());
getDialog().setTitle(GpodnetPreferences.getHosturl());
advance();
});
}
private void setupLoginView(View view) {
final EditText username = view.findViewById(R.id.etxtUsername);
final EditText password = view.findViewById(R.id.etxtPassword);
final Button login = view.findViewById(R.id.butLogin);
final TextView txtvError = view.findViewById(R.id.credentialsError);
final ProgressBar progressBar = view.findViewById(R.id.progBarLogin);
final TextView createAccount = view.findViewById(R.id.createAccountButton);
final TextView createAccountWarning = view.findViewById(R.id.createAccountWarning);
createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/"));
if (GpodnetPreferences.getHosturl().startsWith("http://")) {
createAccountWarning.setVisibility(View.VISIBLE);
}
password.setOnEditorActionListener((v, actionID, event) ->
actionID == EditorInfo.IME_ACTION_GO && login.performClick());
login.setOnClickListener(v -> {
final String usernameStr = username.getText().toString();
final String passwordStr = password.getText().toString();
if (usernameHasUnwantedChars(usernameStr)) {
txtvError.setText(R.string.gpodnetsync_username_characters_error);
txtvError.setVisibility(View.VISIBLE);
return;
}
login.setEnabled(false);
progressBar.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
InputMethodManager inputManager = (InputMethodManager) getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
Completable.fromAction(() -> {
service.authenticate(usernameStr, passwordStr);
devices = service.getDevices();
GpodderAuthenticationFragment.this.username = usernameStr;
GpodderAuthenticationFragment.this.password = passwordStr;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
login.setEnabled(true);
progressBar.setVisibility(View.GONE);
advance();
}, error -> {
login.setEnabled(true);
progressBar.setVisibility(View.GONE);
txtvError.setText(error.getCause().getMessage());
txtvError.setVisibility(View.VISIBLE);
});
});
}
private void setupDeviceView(View view) {
final EditText deviceName = view.findViewById(R.id.deviceName);
final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer);
deviceName.setText(generateDeviceName());
MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton);
createDeviceButton.setOnClickListener(v -> createDevice(view));
for (GpodnetDevice device : devices) {
View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null);
Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton);
selectDeviceButton.setOnClickListener(v -> {
selectedDevice = device;
advance();
});
selectDeviceButton.setText(device.getCaption());
devicesContainer.addView(row);
}
}
private void createDevice(View view) {
final EditText deviceName = view.findViewById(R.id.deviceName);
final TextView txtvError = view.findViewById(R.id.deviceSelectError);
final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice);
String deviceNameStr = deviceName.getText().toString();
if (isDeviceInList(deviceNameStr)) {
return;
}
progBarCreateDevice.setVisibility(View.VISIBLE);
txtvError.setVisibility(View.GONE);
deviceName.setEnabled(false);
Observable.fromCallable(() -> {
String deviceId = generateDeviceId(deviceNameStr);
service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE);
return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(device -> {
progBarCreateDevice.setVisibility(View.GONE);
selectedDevice = device;
advance();
}, error -> {
deviceName.setEnabled(true);
progBarCreateDevice.setVisibility(View.GONE);
txtvError.setText(error.getMessage());
txtvError.setVisibility(View.VISIBLE);
});
}
private String generateDeviceName() {
String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL);
String name = baseName;
int num = 1;
while (isDeviceInList(name)) {
name = baseName + " (" + num + ")";
num++;
}
return name;
}
private String generateDeviceId(String name) {
// devices names must be of a certain form:
// https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices
return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US);
}
private boolean isDeviceInList(String name) {
if (devices == null) {
return false;
}
String id = generateDeviceId(name);
for (GpodnetDevice device : devices) {
if (device.getId().equals(id) || device.getCaption().equals(name)) {
return true;
}
}
return false;
}
private GpodnetDevice findDevice(String id) {
if (devices == null) {
return null;
}
for (GpodnetDevice device : devices) {
if (device.getId().equals(id)) {
return device;
}
}
return null;
}
private void setupFinishView(View view) {
final Button sync = view.findViewById(R.id.butSyncNow);
sync.setOnClickListener(v -> {
dismiss();
SyncService.sync(getContext());
});
}
private void writeLoginCredentials() {
GpodnetPreferences.setUsername(username);
GpodnetPreferences.setPassword(password);
GpodnetPreferences.setDeviceID(selectedDevice.getId());
}
private void advance() {
if (currentStep < STEP_FINISH) {
View view = viewFlipper.getChildAt(currentStep + 1);
if (currentStep == STEP_DEFAULT) {
setupHostView(view);
} else if (currentStep == STEP_HOSTNAME) {
setupLoginView(view);
} else if (currentStep == STEP_LOGIN) {
if (username == null || password == null) {
throw new IllegalStateException("Username and password must not be null here");
} else {
setupDeviceView(view);
}
} else if (currentStep == STEP_DEVICE) {
if (selectedDevice == null) {
throw new IllegalStateException("Device must not be null here");
} else {
writeLoginCredentials();
setupFinishView(view);
}
}
if (currentStep != STEP_DEFAULT) {
viewFlipper.showNext();
}
currentStep++;
} else {
dismiss();
}
}
private boolean usernameHasUnwantedChars(String username) {
Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]");
Matcher containsUnwantedChars = special.matcher(username);
return containsUnwantedChars.find();
}
}

View File

@ -14,19 +14,16 @@ import de.danoeh.antennapod.core.event.SyncServiceEvent;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync";
private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync";
private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname";
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -51,6 +48,7 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void syncStatusChanged(SyncServiceEvent event) {
updateGpodnetPreferenceScreen();
if (!GpodnetPreferences.loggedIn()) {
return;
}
@ -66,6 +64,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
private void setupGpodderScreen() {
final Activity activity = getActivity();
findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> {
new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG);
return true;
});
findPreference(PREF_GPODNET_SETLOGIN_INFORMATION)
.setOnPreferenceClickListener(preference -> {
AuthenticationDialog dialog = new AuthenticationDialog(activity,
@ -94,11 +96,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
updateGpodnetPreferenceScreen();
return true;
});
findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(preference -> {
GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener(
dialog -> updateGpodnetPreferenceScreen());
return true;
});
}
private void updateGpodnetPreferenceScreen() {
@ -119,7 +116,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
} else {
findPreference(PREF_GPODNET_LOGOUT).setSummary(null);
}
findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname());
}
private void updateLastGpodnetSyncReport(boolean successful, long lastTime) {

View File

@ -71,13 +71,13 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat {
Context context = getActivity().getApplicationContext();
String val;
long interval = UserPreferences.getUpdateInterval();
if(interval > 0) {
if (interval > 0) {
int hours = (int) TimeUnit.MILLISECONDS.toHours(interval);
String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours);
val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr);
val = context.getResources().getQuantityString(
R.plurals.pref_autoUpdateIntervallOrTime_every_hours, hours, hours);
} else {
int[] timeOfDay = UserPreferences.getUpdateTimeOfDay();
if(timeOfDay.length == 2) {
if (timeOfDay.length == 2) {
Calendar cal = new GregorianCalendar();
cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]);
cal.set(Calendar.MINUTE, timeOfDay[1]);

View File

@ -9,11 +9,14 @@ import androidx.preference.PreferenceFragmentCompat;
import android.widget.ListView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.dialog.FeedSortDialog;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import org.apache.commons.lang3.ArrayUtils;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
@ -37,8 +40,17 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat {
(preference, newValue) -> {
getActivity().recreate();
return true;
}
);
});
findPreference(UserPreferences.PREF_SHOW_TIME_LEFT)
.setOnPreferenceChangeListener(
(preference, newValue) -> {
UserPreferences.setShowRemainTimeSetting((Boolean) newValue);
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
EventBus.getDefault().post(new PlayerStatusEvent());
return true;
});
findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)
.setOnPreferenceClickListener(preference -> {
showDrawerPreferencesDialog();

View File

@ -9,7 +9,7 @@ import androidx.appcompat.widget.SearchView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.fragment.SearchFragment;
import java.util.HashMap;

View File

@ -2,6 +2,7 @@ package de.danoeh.antennapod.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.KeyEvent;
import androidx.preference.PreferenceManager;
import de.danoeh.antennapod.BuildConfig;
@ -92,5 +93,16 @@ public class PreferenceUpgrader {
if (oldVersion < 1080100) {
prefs.edit().putString(UserPreferences.PREF_VIDEO_BEHAVIOR, "pip").apply();
}
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();
}
}
}
}

View File

@ -0,0 +1,129 @@
package de.danoeh.antennapod.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import de.danoeh.antennapod.ui.common.ThemeUtils;
public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar {
private float top;
private float width;
private float bottom;
private float density;
private float progressPrimary;
private float progressSecondary;
private float[] dividerPos;
private final Paint paintBackground = new Paint();
private final Paint paintProgressPrimary = new Paint();
private final Paint paintProgressSecondary = new Paint();
public ChapterSeekBar(Context context) {
super(context);
init(context);
}
public ChapterSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setBackground(null); // Removes the thumb shadow
dividerPos = null;
density = context.getResources().getDisplayMetrics().density;
paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(),
de.danoeh.antennapod.core.R.attr.currently_playing_background));
paintBackground.setAlpha(128);
paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(),
de.danoeh.antennapod.core.R.attr.colorPrimary));
paintProgressSecondary.setColor(ThemeUtils.getColorFromAttr(getContext(),
de.danoeh.antennapod.core.R.attr.seek_background));
}
/**
* Sets the relative positions of the chapter dividers.
* @param dividerPos of the chapter dividers relative to the duration of the media.
*/
public void setDividerPos(final float[] dividerPos) {
if (dividerPos != null) {
this.dividerPos = new float[dividerPos.length + 2];
this.dividerPos[0] = 0;
System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length);
this.dividerPos[this.dividerPos.length - 1] = 1;
} else {
this.dividerPos = null;
}
}
@Override
protected synchronized void onDraw(Canvas canvas) {
top = getTop() + density * 7.5f;
bottom = getBottom() - density * 7.5f;
width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft());
progressSecondary = getSecondaryProgress() / (float) getMax() * width;
progressPrimary = getProgress() / (float) getMax() * width;
if (dividerPos == null) {
drawProgress(canvas);
} else {
drawProgressChapters(canvas);
}
drawThumb(canvas);
}
private void drawProgress(Canvas canvas) {
final int saveCount = canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
canvas.drawRect(0, top, width, bottom, paintBackground);
canvas.drawRect(0, top, progressSecondary, bottom, paintProgressSecondary);
canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary);
canvas.restoreToCount(saveCount);
}
private void drawProgressChapters(Canvas canvas) {
final int saveCount = canvas.save();
int currChapter = 1;
float chapterMargin = density * 0.6f;
float topExpanded = getTop() + density * 7;
float bottomExpanded = getBottom() - density * 7;
canvas.translate(getPaddingLeft(), getPaddingTop());
for (int i = 1; i < dividerPos.length; i++) {
float right = dividerPos[i] * width - chapterMargin;
float left = dividerPos[i - 1] * width + chapterMargin;
float rightCurr = dividerPos[currChapter] * width - chapterMargin;
float leftCurr = dividerPos[currChapter - 1] * width + chapterMargin;
canvas.drawRect(left, top, right, bottom, paintBackground);
if (right < progressPrimary) {
currChapter = i + 1;
canvas.drawRect(left, top, right, bottom, paintProgressPrimary);
} else if (isPressed()) {
canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground);
canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary);
} else {
if (progressSecondary > leftCurr) {
canvas.drawRect(leftCurr, top, progressSecondary, bottom, paintProgressSecondary);
}
canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary);
}
}
canvas.restoreToCount(saveCount);
}
private void drawThumb(Canvas canvas) {
final int saveCount = canvas.save();
canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop());
getThumb().draw(canvas);
canvas.restoreToCount(saveCount);
}
}

View File

@ -6,9 +6,9 @@ import android.content.res.Configuration;
import android.util.AttributeSet;
import android.view.View;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import io.reactivex.annotations.Nullable;
@ -39,7 +39,7 @@ public class EpisodeItemListRecyclerView extends RecyclerView {
layoutManager.setRecycleChildrenOnDetach(true);
setLayoutManager(layoutManager);
setHasFixedSize(true);
addItemDecoration(new HorizontalDividerItemDecoration.Builder(getContext()).build());
addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation()));
setClipToPadding(false);
}

View File

@ -20,6 +20,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder {
public final TextView type;
public final TextView date;
public final TextView reason;
public final TextView tapForDetails;
public DownloadItemViewHolder(Context context, ViewGroup parent) {
super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false));
@ -27,6 +28,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder {
type = itemView.findViewById(R.id.txtvType);
icon = itemView.findViewById(R.id.txtvIcon);
reason = itemView.findViewById(R.id.txtvReason);
tapForDetails = itemView.findViewById(R.id.txtvTapForDetails);
secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton);
secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon);
title = itemView.findViewById(R.id.txtvTitle);

View File

@ -13,9 +13,7 @@ import android.widget.TextView;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import com.joanzapata.iconify.Iconify;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.CoverLoader;
@ -25,13 +23,15 @@ import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.view.CircularProgressBar;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.ui.common.CircularProgressBar;
/**
* Holds the view which shows FeedItems.
@ -121,8 +121,8 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
if (coverHolder.getVisibility() == View.VISIBLE) {
new CoverLoader(activity)
.withUri(ImageResourceUtils.getImageLocation(item))
.withFallbackUri(item.getFeed().getImageLocation())
.withUri(ImageResourceUtils.getEpisodeListImageLocation(item))
.withFallbackUri(item.getFeed().getImageUrl())
.withPlaceholderView(placeholder)
.withCoverView(cover)
.load();
@ -132,9 +132,6 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
private void bind(FeedMedia media) {
isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE);
duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE);
duration.setText(Converter.getDurationStringLong(media.getDuration()));
duration.setContentDescription(activity.getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(activity, media.getDuration())));
if (media.isCurrentlyPlaying()) {
itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background));
@ -152,6 +149,9 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0%
}
duration.setText(Converter.getDurationStringLong(media.getDuration()));
duration.setContentDescription(activity.getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(activity, media.getDuration())));
if (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS) {
int progress = (int) (100.0 * media.getPosition() / media.getDuration());
progressBar.setProgress(progress);
@ -160,6 +160,11 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
Converter.getDurationStringLocalized(activity, media.getPosition())));
progressBar.setVisibility(View.VISIBLE);
position.setVisibility(View.VISIBLE);
if (UserPreferences.shouldShowRemainingTime()) {
duration.setText("-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition()));
duration.setContentDescription(activity.getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition()))));
}
} else {
progressBar.setVisibility(View.GONE);
position.setVisibility(View.GONE);
@ -186,6 +191,22 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
}
}
private void updateDuration(PlaybackPositionEvent event) {
int currentPosition = event.getPosition();
int timeDuration = event.getDuration();
int remainingTime = event.getDuration() - event.getPosition();
Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == PlaybackService.INVALID_TIME || timeDuration == PlaybackService.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time");
return;
}
if (UserPreferences.shouldShowRemainingTime()) {
duration.setText("-" + Converter.getDurationStringLong(remainingTime));
} else {
duration.setText(Converter.getDurationStringLong(timeDuration));
}
}
public FeedItem getFeedItem() {
return item;
}
@ -197,7 +218,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) {
progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration()));
position.setText(Converter.getDurationStringLong(event.getPosition()));
duration.setText(Converter.getDurationStringLong(event.getDuration()));
updateDuration(event);
duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known
}

View File

@ -2,9 +2,9 @@ AntennaPod je správce a přehrávač podcastů, co vám umožňuje okamžitý p
Stahujte, streamujte nebo si vytvořte frontu epizod a užijte si poslech tak, jak ho máte rádi s nastavitelnou rychlostí přehrávání, podporou kapitol a s časovačem vypnutí.
Ušetři si námahu, baterku i mobilní data s pomocí robustní automatické kontroly nad stahováním epizod (urči časy, intervaly a WIFI sítě) a mazáním epizod (na základě oblíbenosti a nastavení zpoždění).
Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdarma a bez reklam.
Vytvořeno nadšenci do podcastů, AntennaPod je software s otevřeným zdrojovým kódem, zdarma a bez reklam.
<b>Importujte, zorganizujte a přehrávejte</b>
<b>Importujte, organizujte a přehrávejte</b>
• Ovládejte přehrávání odkudkoli: z widgetu na domovské obrazovce, z oznámení nebo pomocí tlačítek na sluchátkách včetně Bluetooth
• Přidejte a importujte podcasty přes iTunes anebo gPodder.net, OPML soubory a RSS anebo Atom odkazy
• Užijte si poslech s nastavitelnou rychlostí přehrávání, podporou kapitol, zapamatování poslední pozice přehrávání a pokročilým časovačem vypnutí (restart zatřesením, snížení hlasitosti)
@ -21,7 +21,7 @@ Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdar
• Přizpůsobte si aplikaci svému prostředí pomocí světlého nebo tmavého motivu
• Zálohujte své sbírky pomocí služby gPodder.net nebo exportem OPML souborů
<b>Přidejte se do komunity AntennaPodu!</b>
<b>Přidejte se ke komunitě AntennaPod!</b>
AntennaPod je aktivně vyvíjen dobrovolníky. Můžete přispět také svým kódem nebo komentáři!
Naše přátelská členská základna vám ráda zodpoví jakékoli dotazy. Zveme vás k diskuzi o AntennaPodu nebo i podcastech obecně.

View File

@ -1,4 +1,4 @@
AntennaPod ist ein Podcast-Manager und -Player, der dir unmittelbar Zugriff auf Millionen von freien und bezahlten Podcasts ermöglicht. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Verzeichnisses, OPML-Dateien oder einfachen RSS-URLs.
AntennaPod ist ein Podcast-Manager und -Player, mit dem Du direkten Zugriff auf Millionen von freien und kostenpflichtigen Podcasts hast. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Podcast-Verzeichnisses, OPML-Dateien oder RSS-URLs.
Downloade, streame oder sortiere Episoden in der Abspielliste und genieße sie mit einstellbarer Abspielgeschwindigkeit, Unterstützung von Kapiteln und Schlummerfunktion.
Reduziere Aufwand, Stromverbrauch und Datenverbrauch durch leistungsfähige Kontrolle der Downloads (bestimmte Uhrzeiten, Intervalle, WiFi-Netze) und des Löschens (basierend auf deinen Favoriten und weiteren Einstellungen).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -24,8 +24,8 @@ Dodawaj i importuj kanały z iTunes i gPodder.net, plików OPML oraz z adresów
<b>Dołącz do społeczności AntennaPod</b>
AntennaPod jest ciągle rozwijane przez ochotników. Ty też możesz pomóc, kodem lub komentarzem!
Chcesz zgłosić błąd lub brakuje Ci jakiejś funkcji, a może programujesz? Odwiedź nasz GitHub:
https://www.github.com/AntennaPod/AntennaPod
Życzliwi użytkownicy forum chętnie odpowiedzą na twoje pytania. Zapraszamy do rozmów o funkcjach programu i generalnie o podcastingu.
https://forum.antennapod.org/
Chcesz pomóc tłumaczyć AntennaPod - możesz to zrobić na Transifex:
https://www.transifex.com/antennapod/antennapod

View File

@ -0,0 +1,31 @@
AntennaPod je správca a prehrávač podcastov, ktorý vám sprostredkuje okamžitý prístup k miliónom bezplatným a spoplatneným podcastom - od nezávislých podcastérov až k vydavateľstvám ako BBC, NPR a CNN. Pridávajte, importujte a exportujte ich zdroje bez problémov pomocou databázy podcastov iTunes, OPML súborov alebo odkazov na RSS.
Stiahnite, streamujte alebo plánujte epizódy a užívajte si ich ako sa vám páči s nastaviteľnou rýchlosťou prehrávania, podporou kapitol a časovačom vypnutia.
Ušetrite si námahu, baterku a mobilné dáta pomocou výkonnej automatickej kontroly sťahovania epizód (určite čas, intervaly a WiFi siete) a mazania epizód (založené na obľúbených epizódach a nastavení oneskorenia).
AntennaPod je spravovaný podcastovými entuziastami a je slobodný v každom slova zmysle: otvorený zdrojový kód, zadarmo, bez reklamy.
<b>Importovať, spravovať a prehrať</b>
• Ovládajte prehrávanie odkiaľkoľvek: domovská obrazovka, systémové upozornenia a handsfree a ovládanie cez bluetooth
• Pridať a importovať zdroje pomocou priečinkov iTunes a gPodder.net, OPML súborov a odkazov RSS alebo Atom
• Užívajte si počúvanie s nastaviteľnou rýchlosťou prehrávania, podporou kapitol, zapamätanou pozíciou prehrávania a pokročilým nastavením vypnutia (zatrasením resetuj, stíš hlasitosť)
Počúvajte heslom chránené zdroje a epizódy
<b>Sledovať, zdielať a oceniť</b>
• Sledujte najlepšie z najlepších pomocou označenia ako obľúbené
• Nájdite epizódu v histórií prehrávaní alebo v nadpisoch a popisoch
• Zdielajte epizódy a zdroje pomocou pokročilých nastavení sociálnych sietí a e-mailu, služieb gPodder.net a cez export do OPML súboru
<b>Spravovať systém</b>
• Spravujte automatické sťahovanie: vyberte zdroje, vylúčte mobilné siete, vyberte konkrétne WiFi siete, vyžadujte nabíjanie telefónu a nastavte časy a intervaly
• Spravujte úložisko nastavením počtu uložených epizód, inteligentným mazaním a zvolením umiestnenia súborov
• Prispôsobte si vzhľad na svetlý alebo tmavý
• Zálohujte si odbery pomocou gPodder.net a OPML exportu
<b>Pridať sa do komunity AntennaPod!</b>
AntennaPod aktívne vyvíjajú dobrovoľníci. Tiež môžete prispieť kódom alebo komentárom.
Na našom fóre môžete v priateľskej atmosfére diskutovať a nájsť odpovede na vaše otázky o nových funkciách alebo všeobecne o podcastingu.
https://forum.antennapod.org/
Transifex je miesto, kde môžete pomôcť s prekladom:
https://www.transifex.com/antennapod/antennapod

View File

@ -0,0 +1 @@
Ľahko použíteľný, flexibilný a open source správca a prehrávač podcastov

View File

@ -0,0 +1 @@
AntennaPod

View File

@ -1,7 +1,12 @@
- A long-standing wish of many: playing local files! In the 'Add podcast' screen simply tap 'Add local folder' and select a location on your phone! (@ByteHamster, @igoralmeida & @damoasda)
- Pick a country for the 'Discover' screen (@tonytamsf)
- Keyboard shortcuts (@asdoi)
- Search the PodcastIndex.org database (@edwinhere)
- Pull to refresh (@asdoi)
- Playback speed & filter dialogs (@ByteHamster & @bws9000)
- Smooth sleep timer volume (@olivoto)
NEW
- Optional notifications for new episodes (@connectety)
- Use PodcastIndex for main search (@tonytamsf)
- Sleep timer extend buttons (@max-wittig)
- Optional rewind, forward & skip buttons on widget (@tonytamsf)
- 'When not favorited' as Episode Cleanup (@spacecowboy)
IMPROVED
- More actions for hardware buttons (@timakro)
- Android Auto & chapter support (@tonytamsf, @ByteHamster)
- Fixed stuck notification (@a1291762)
- Player screen usability for visually impaired (@ByteHamster)

View File

@ -22,7 +22,7 @@
android:id="@+id/widget_config_preview"
layout="@layout/player_widget"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:layout_margin="16dp" />
</FrameLayout>
@ -68,13 +68,38 @@
android:max="100"
android:progress="100" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/ckRewind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Rewind" />
<CheckBox
android:id="@+id/ckFastForward"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Forward" />
<CheckBox
android:id="@+id/ckSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Skip" />
</LinearLayout>
<Button
android:id="@+id/butConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/widget_create_button" />
</LinearLayout>
</LinearLayout>

View File

@ -51,6 +51,34 @@
app:tint="?android:attr/windowBackground"
android:importantForAccessibility="no"/>
<androidx.cardview.widget.CardView
android:id="@+id/cardViewSeek"
android:alpha="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/pager"
android:layout_centerHorizontal="true"
android:layout_marginBottom="12dp"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="?attr/seek_background"
app:cardElevation="0dp"
tools:alpha="1">
<TextView
android:id="@+id/txtvSeek"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingTop="4dp"
android:paddingRight="24dp"
android:paddingBottom="4dp"
android:textColor="@color/white"
android:textSize="24sp"
tools:text="1:06:29" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/playtime_layout"
android:layout_width="match_parent"
@ -60,7 +88,7 @@
android:layoutDirection="ltr"
android:orientation="vertical">
<SeekBar
<de.danoeh.antennapod.view.ChapterSeekBar
android:id="@+id/sbPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -128,7 +156,7 @@
android:scaleType="fitCenter"
tools:srcCompat="@drawable/ic_av_play_white_24dp"/>
<de.danoeh.antennapod.view.CircularProgressBar
<de.danoeh.antennapod.ui.common.CircularProgressBar
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
android:layout_marginLeft="16dp"
@ -136,7 +164,8 @@
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"/>
android:layout_centerVertical="true"
app:foregroundColor="?attr/action_icon_color"/>
<ProgressBar
style="?android:attr/progressBarStyle"
@ -177,7 +206,7 @@
android:textColor="?android:attr/textColorSecondary"
android:clickable="false"/>
<de.danoeh.antennapod.view.PlaybackSpeedIndicatorView
<de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView
android:id="@+id/butPlaybackSpeed"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
@ -186,7 +215,8 @@
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/playback_speed"
tools:srcCompat="@drawable/ic_playback_speed_white"/>
tools:srcCompat="@drawable/ic_playback_speed_white"
app:foregroundColor="?attr/action_icon_color"/>
<TextView
android:id="@+id/txtvPlaybackSpeed"

View File

@ -1,30 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<EditText
android:id="@+id/etxtUsername"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
android:hint="@string/username_label"
android:focusable="true"
android:focusableInTouchMode="true"
android:cursorVisible="true"/>
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/etxtPassword"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
android:inputType="textPassword"
android:hint="@string/password_label"
android:focusable="true"
android:focusableInTouchMode="true"
android:cursorVisible="true"/>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/usernameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username_label"
android:lines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password_label"
android:inputType="textPassword"
android:lines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<com.joanzapata.iconify.widget.IconTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/showPasswordButton"
android:text="{fa-eye}"
android:padding="8dp"
android:textColor="?android:attr/textColorPrimary"
android:background="?attr/selectableItemBackgroundBorderless"
android:alpha="0.6"
android:textSize="20sp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
</LinearLayout>
</LinearLayout>

View File

@ -10,7 +10,7 @@
android:padding="8dp"
android:gravity="center">
<de.danoeh.antennapod.view.SquareImageView
<de.danoeh.antennapod.ui.common.SquareImageView
android:id="@+id/imgvCover"
android:layout_width="0dp"
android:layout_height="200dp"

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_notification_title"
android:textSize="@dimen/text_size_large"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/txtvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_notification_msg"
android:textColor="?android:attr/textColorSecondary"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/etxtUsername"
android:hint="@string/username_label"
android:focusable="true"
android:focusableInTouchMode="true"
android:cursorVisible="true"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/etxtPassword"
android:hint="@string/password_label"
android:inputType="textPassword"
android:focusable="true"
android:focusableInTouchMode="true"
android:cursorVisible="true"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/butCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_label"
style="@style/Widget.MaterialComponents.Button.TextButton"/>
<Button
android:id="@+id/butConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/confirm_label"
style="@style/Widget.MaterialComponents.Button.TextButton"/>
</LinearLayout>
</LinearLayout>

View File

@ -83,6 +83,14 @@
android:textColor="?android:attr/textColorSecondary"
tools:text="@string/design_time_downloaded_log_failure_reason"/>
<TextView
android:id="@+id/txtvTapForDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:text="@string/download_error_tap_for_details"/>
</LinearLayout>
<include layout="@layout/secondary_action"/>

View File

@ -94,17 +94,6 @@
</LinearLayout>
<TextView
android:id="@+id/txtvInformation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:background="?android:attr/windowBackground"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible"
tools:text="(i) Information"/>
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/txtvFailure"
android:layout_width="match_parent"
@ -117,4 +106,15 @@
android:text="@string/refresh_failed_msg"
tools:visibility="visible"
tools:text="(!) Last refresh failed"/>
<TextView
android:id="@+id/txtvInformation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:background="?android:attr/windowBackground"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible"
tools:text="(i) Information"/>
</LinearLayout>

View File

@ -145,6 +145,7 @@
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
android:text="·"
android:importantForAccessibility="no"
tools:background="@android:color/holo_blue_light"/>
<TextView
@ -163,6 +164,7 @@
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
android:text="·"
android:importantForAccessibility="no"
tools:background="@android:color/holo_blue_light"/>
<TextView

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