Merge branch 'develop' into develop

This commit is contained in:
Martin Fietz 2017-04-09 20:34:43 +02:00 committed by GitHub
commit 5cbf092f3b
146 changed files with 3751 additions and 1110 deletions

View File

@ -4,6 +4,7 @@ host = https://www.transifex.com
[antennapod.english]
source_file = core/src/main/res/values/strings.xml
source_lang = en
trans.ast_ES = core/src/main/res/values-b+ast/strings.xml
trans.ar = core/src/main/res/values-ar/strings.xml
trans.az = core/src/main/res/values-az/strings.xml
trans.ca = core/src/main/res/values-ca/strings.xml
@ -27,6 +28,7 @@ trans.ja = core/src/main/res/values-ja/strings.xml
trans.kn_IN = core/src/main/res/values-kn-rIN/strings.xml
trans.ko = core/src/main/res/values-ko/strings.xml
trans.ko_KR = core/src/main/res/values-ko-rKR/strings.xml
trans.lt = core/src/main/res/values-lt/strings.xml
trans.nb = core/src/main/res/values-nb/strings.xml
trans.no = core/src/main/res/values-no/strings.xml
trans.nl = core/src/main/res/values-nl/strings.xml
@ -42,9 +44,11 @@ trans.ru_RU = core/src/main/res/values-ru/strings.xml
trans.uk_UA = core/src/main/res/values-uk-rUA/strings.xml
trans.zh_CN = core/src/main/res/values-zh-rCN/strings.xml
trans.sv_SE = core/src/main/res/values-sv-rSE/strings.xml
trans.te = core/src/main/res/values-te/strings.xml
trans.tr = core/src/main/res/values-tr/strings.xml
trans.vi = core/src/main/res/values-vi/strings.xml
trans.vi_VN = core/src/main/res/values-vi-rVN/strings.xml
trans.zh_TW = core/src/main/res/values-zh-rTW/strings.xml
[antennapod.description]
file_filter = app/src/main/play/<lang>/listing/fulldescription

389
CHANGELOG.md Normal file
View File

@ -0,0 +1,389 @@
Change Log
==========
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

@ -21,7 +21,7 @@ AntennaPod has many users and we don't want them to run into trouble when we add
There are APKs available for every branch that is actively worked on. Please note that these might be very unstable versions of the app, which can break your current installation. Install them at your own risk!
Click [here](https://www.dropbox.com/sh/4h2kfa2d2jesnu8/AABF7u5QsOn8Lg8MDie4Z4Ama?dl=0) to get to the nightly builds folder.
Click [here](https://www.dropbox.com/sh/lzfd640z63qz3fr/AACyxTF1ygR9wMlPLPwVGIUKa?dl=0) to get to the nightly builds folder.
## License

View File

@ -107,6 +107,10 @@ android {
play {
}
}
dexOptions {
jumboMode true
}
}
configurations {
@ -139,8 +143,8 @@ dependencies {
compile "commons-io:commons-io:$commonsioVersion"
compile "org.jsoup:jsoup:$jsoupVersion"
compile "com.github.bumptech.glide:glide:$glideVersion"
compile "com.squareup.okhttp:okhttp:$okhttpVersion"
compile "com.squareup.okhttp:okhttp-urlconnection:$okhttpVersion"
compile "com.squareup.okhttp3:okhttp:$okhttpVersion"
compile "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
compile "com.squareup.okio:okio:$okioVersion"
compile "de.greenrobot:eventbus:$eventbusVersion"
compile "io.reactivex:rxandroid:$rxAndroidVersion"
@ -160,6 +164,8 @@ dependencies {
compile "com.github.shts:TriangleLabelView:$triangleLabelViewVersion"
compile "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
compile 'com.github.mfietz:fyydlin:v0.1'
}
play {
@ -178,7 +184,8 @@ task filterAbout {
from "src/main/templates/about.html"
into "src/main/assets"
filter(ReplaceTokens, tokens: [versionname: android.defaultConfig.versionName,
commit : "git rev-parse --short HEAD".execute().text])
commit : "git rev-parse --short HEAD".execute().text,
year : new Date().format('yyyy')])
}
}

View File

@ -53,7 +53,7 @@
public *;
}
-dontwarn com.squareup.okhttp.**
-dontwarn okhttp3.**
-dontwarn okio.**
# for RxJava:
@ -107,4 +107,19 @@
-dontwarn com.viewpagerindicator.LinePageIndicator
# for some reason ProGuard removes this file. Why? Unsure.
-keep class de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider { *; }
-keep class de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider { *; }
# Retrofit 2.0
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Moshi
-keep class com.squareup.moshi.** { *; }
-keep interface com.squareup.moshi.** { *; }
-keep public class retrofit2.adapter.rxjava.RxJavaCallAdapterFactory { *; }

View File

@ -789,7 +789,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext) {
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
}

View File

@ -44,7 +44,7 @@ public class DBTestUtils {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
for (int i = 0; i < numFeeds; i++) {
Feed f = new Feed(0, null, "feed " + i, "link" + i, "descr", null, null,
Feed f = new Feed(0, null, "feed " + i, null, "link" + i, "descr", null, null,
null, null, "id" + i, null, null, "url" + i, false, new FlattrStatus(), false, null, null, false);
f.setItems(new ArrayList<>());
for (int j = 0; j < numItems; j++) {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.danoeh.antennapod"
android:versionCode="1060102"
android:versionName="1.6.1.2">
android:versionCode="1060203"
android:versionName="1.6.2.3">
<!--
Version code schema:
"1.2.3-SNAPSHOT" -> 1020300
@ -37,22 +37,27 @@
android:backupAgent=".core.backup.OpmlBackupAgent"
android:restoreAnyVersion="true"
android:logo="@drawable/ic_launcher">
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/ic_notification" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
<activity
android:name=".activity.MainActivity"
android:name=".activity.SplashActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:label="@string/app_name">
android:label="@string/app_name"
android:theme="@style/Theme.AntennaPod.Dark.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:label="@string/app_name">
</activity>
<activity
android:name=".activity.AudioplayerActivity"
android:launchMode="singleTop">
@ -344,10 +349,6 @@
<meta-data
android:name="de.danoeh.antennapod.core.glide.ApGlideModule"
android:value="GlideModule" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
</application>
</manifest>

View File

@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
@ -27,6 +28,10 @@ import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.joanzapata.iconify.Iconify;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
@ -40,6 +45,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LangUtils;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import rx.Observable;
import rx.Subscription;
@ -50,17 +56,18 @@ import rx.schedulers.Schedulers;
* Displays information about a feed.
*/
public class FeedInfoActivity extends AppCompatActivity {
private static final String TAG = "FeedInfoActivity";
private boolean autoDeleteChanged = false;
public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedInfoActivity";
private boolean autoDeleteChanged = false;
private Feed feed;
private ImageView imgvCover;
private TextView txtvTitle;
private TextView txtvDescription;
private TextView lblLanguage;
private TextView txtvLanguage;
private TextView lblAuthor;
private TextView txtvAuthor;
private TextView txtvUrl;
private EditText etxtUsername;
@ -75,6 +82,7 @@ public class FeedInfoActivity extends AppCompatActivity {
private Subscription subscription;
private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -96,6 +104,40 @@ public class FeedInfoActivity extends AppCompatActivity {
}
};
private boolean authInfoChanged = false;
private TextWatcher authTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
authInfoChanged = true;
}
};
private boolean filterTextChanged = false;
private TextWatcher filterTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
filterTextChanged = true;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
@ -107,7 +149,9 @@ public class FeedInfoActivity extends AppCompatActivity {
imgvCover = (ImageView) findViewById(R.id.imgvCover);
txtvTitle = (TextView) findViewById(R.id.txtvTitle);
txtvDescription = (TextView) findViewById(R.id.txtvDescription);
lblLanguage = (TextView) findViewById(R.id.lblLanguage);
txtvLanguage = (TextView) findViewById(R.id.txtvLanguage);
lblAuthor = (TextView) findViewById(R.id.lblAuthor);
txtvAuthor = (TextView) findViewById(R.id.txtvAuthor);
txtvUrl = (TextView) findViewById(R.id.txtvUrl);
cbxAutoDownload = (CheckBox) findViewById(R.id.cbxAutoDownload);
@ -152,14 +196,30 @@ public class FeedInfoActivity extends AppCompatActivity {
.into(imgvCover);
txtvTitle.setText(feed.getTitle());
String description = feed.getDescription();
txtvDescription.setText((description != null) ? description.trim() : "");
if (feed.getAuthor() != null) {
txtvAuthor.setText(feed.getAuthor());
if(description != null) {
if(Feed.TYPE_ATOM1.equals(feed.getType())) {
HtmlToPlainText formatter = new HtmlToPlainText();
Document feedDescription = Jsoup.parse(feed.getDescription());
description = StringUtils.trim(formatter.getPlainText(feedDescription));
}
} else {
description = "";
}
if (feed.getLanguage() != null) {
txtvLanguage.setText(LangUtils
.getLanguageString(feed.getLanguage()));
txtvDescription.setText(description);
if (!TextUtils.isEmpty(feed.getAuthor())) {
txtvAuthor.setText(feed.getAuthor());
} else {
lblAuthor.setVisibility(View.GONE);
txtvAuthor.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(feed.getLanguage())) {
txtvLanguage.setText(LangUtils.getLanguageString(feed.getLanguage()));
} else {
lblLanguage.setVisibility(View.GONE);
txtvLanguage.setVisibility(View.GONE);
}
txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}");
Iconify.addIcons(txtvUrl);
@ -240,53 +300,6 @@ public class FeedInfoActivity extends AppCompatActivity {
});
}
@Override
public void onDestroy() {
super.onDestroy();
if(subscription != null) {
subscription.unsubscribe();
}
}
private boolean authInfoChanged = false;
private TextWatcher authTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
authInfoChanged = true;
}
};
private boolean filterTextChanged = false;
private TextWatcher filterTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
filterTextChanged = true;
}
};
@Override
protected void onPause() {
super.onPause();
@ -318,6 +331,14 @@ public class FeedInfoActivity extends AppCompatActivity {
}
}
@Override
public void onDestroy() {
super.onDestroy();
if(subscription != null) {
subscription.unsubscribe();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
@ -369,7 +390,7 @@ public class FeedInfoActivity extends AppCompatActivity {
private final Feed feed;
private final boolean autoDownload;
public ApplyToEpisodesDialog(Context context, Feed feed, boolean autoDownload) {
ApplyToEpisodesDialog(Context context, Feed feed, boolean autoDownload) {
super(context, R.string.auto_download_apply_to_items_title,
R.string.auto_download_apply_to_items_message);
this.feed = feed;

View File

@ -10,6 +10,7 @@ import android.database.DataSetObserver;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
@ -38,6 +39,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.event.ProgressEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
@ -46,11 +48,13 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.Flavors;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.DownloadsFragment;
import de.danoeh.antennapod.fragment.EpisodesFragment;
@ -463,6 +467,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
protected void onResume() {
super.onResume();
StorageUtils.checkStorageAvailability(this);
DBTasks.checkShouldRefreshFeeds(getApplicationContext());
Intent intent = getIntent();
if (intent.hasExtra(EXTRA_FEED_ID) ||
@ -573,6 +578,9 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
case R.id.mark_all_read_item:
DBWriter.markFeedRead(feed.getId());
return true;
case R.id.rename_item:
new RenameFeedDialog(this, feed).show();
return true;
case R.id.remove_item:
final FeedRemover remover = new FeedRemover(this, feed) {
@Override
@ -731,6 +739,18 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
}
}
public void onEventMainThread(MessageEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
View parentLayout = findViewById(R.id.drawer_layout);
Snackbar snackbar = Snackbar.make(parentLayout, event.message, Snackbar.LENGTH_SHORT);
if(event.action != null) {
snackbar.setAction(getString(R.string.undo), v -> {
event.action.run();
});
}
snackbar.show();
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override

View File

@ -8,6 +8,7 @@ import android.content.res.Configuration;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
@ -36,6 +37,7 @@ import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedMedia;
@ -43,9 +45,11 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.ChaptersFragment;
import de.danoeh.antennapod.fragment.CoverFragment;
@ -57,6 +61,7 @@ import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.danoeh.antennapod.preferences.PreferenceController;
import de.greenrobot.event.EventBus;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
@ -101,6 +106,12 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
private Subscription subscription;
@Override
protected void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
}
@Override
protected void onStop() {
super.onStop();
@ -168,8 +179,10 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
pagerAdapter.onMediaChanged(media);
pagerAdapter.setController(controller);
}
DBTasks.checkShouldRefreshFeeds(getApplicationContext());
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadData();
}
@ -370,6 +383,9 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
case R.id.mark_all_read_item:
DBWriter.markFeedRead(feed.getId());
return true;
case R.id.rename_item:
new RenameFeedDialog(this, feed).show();
return true;
case R.id.remove_item:
final FeedRemover remover = new FeedRemover(this, feed) {
@Override
@ -462,7 +478,17 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
public void onEventMainThread(MessageEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
View parentLayout = findViewById(R.id.drawer_layout);
Snackbar snackbar = Snackbar.make(parentLayout, event.message, Snackbar.LENGTH_SHORT);
if (event.action != null) {
snackbar.setAction(getString(R.string.undo), v -> {
event.action.run();
});
}
snackbar.show();
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {

View File

@ -29,7 +29,6 @@ import com.bumptech.glide.Glide;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.examples.HtmlToPlainText;
import org.jsoup.nodes.Document;
import java.io.File;
@ -63,6 +62,7 @@ import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.greenrobot.event.EventBus;
import rx.Observable;
@ -81,17 +81,12 @@ import rx.schedulers.Schedulers;
*/
public class OnlineFeedViewActivity extends AppCompatActivity {
private static final String TAG = "OnlineFeedViewActivity";
public static final String ARG_FEEDURL = "arg.feedurl";
// Optional argument: specify a title for the actionbar.
public static final String ARG_TITLE = "title";
private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE;
public static final int RESULT_ERROR = 2;
private static final String TAG = "OnlineFeedViewActivity";
private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE;
private volatile List<Feed> feeds;
private Feed feed;
private String selectedDownloadUrl;
@ -106,17 +101,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
private Subscription download;
private Subscription parser;
private Subscription updater;
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
setSubscribeButtonState(feed);
}
private EventDistributor.EventListener listener = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) {
updater = Observable.fromCallable(() -> DBReader.getFeedList())
updater = Observable.fromCallable(DBReader::getFeedList)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@ -133,6 +122,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
}
};
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
setSubscribeButtonState(feed);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
@ -284,7 +278,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(status -> checkDownloadResult(status),
.subscribe(this::checkDownloadResult,
error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@ -360,14 +354,19 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
* This method is executed on a background thread
*/
private void beforeShowFeedInformation(Feed feed) {
// remove HTML tags from descriptions
final HtmlToPlainText formatter = new HtmlToPlainText();
if(Feed.TYPE_ATOM1.equals(feed.getType()) && feed.getDescription() != null) {
// remove HTML tags from descriptions
Log.d(TAG, "Removing HTML from feed description");
Document feedDescription = Jsoup.parse(feed.getDescription());
feed.setDescription(StringUtils.trim(formatter.getPlainText(feedDescription)));
}
Log.d(TAG, "Removing HTML from shownotes");
if (feed.getItems() != null) {
HtmlToPlainText formatter = new HtmlToPlainText();
for (FeedItem item : feed.getItems()) {
if (item.getDescription() != null) {
Document description = Jsoup.parse(item.getDescription());
item.setDescription(StringUtils.trim(formatter.getPlainText(description)));
Document itemDescription = Jsoup.parse(item.getDescription());
item.setDescription(StringUtils.trim(formatter.getPlainText(itemDescription)));
}
}
}
@ -589,7 +588,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
private String feedUrl;
public FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) {
FeedViewAuthenticationDialog(Context context, int titleRes, String feedUrl) {
super(context, titleRes, true, false, null, null);
this.feedUrl = feedUrl;
}

View File

@ -15,7 +15,7 @@ import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.preferences.UserPreferences;
/**
@ -23,10 +23,8 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
* which feeds he wants to import.
*/
public class OpmlFeedChooserActivity extends AppCompatActivity {
private static final String TAG = "OpmlFeedChooserActivity";
public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems";
private static final String TAG = "OpmlFeedChooserActivity";
private Button butConfirm;
private Button butCancel;
private ListView feedlist;

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
@ -20,7 +21,7 @@ import java.util.ArrayList;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.OpmlFeedQueuer;
import de.danoeh.antennapod.asynctask.OpmlImportWorker;
import de.danoeh.antennapod.core.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.util.LangUtils;
/**
@ -29,9 +30,8 @@ import de.danoeh.antennapod.core.util.LangUtils;
public class OpmlImportBaseActivity extends AppCompatActivity {
private static final String TAG = "OpmlImportBaseActivity";
private OpmlImportWorker importWorker;
private static final int PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 5;
private OpmlImportWorker importWorker;
@Nullable private Uri uri;
/**
@ -77,7 +77,8 @@ public class OpmlImportBaseActivity extends AppCompatActivity {
return;
}
this.uri = uri;
if(uri.toString().contains(Environment.getExternalStorageDirectory().toString())) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
uri.toString().contains(Environment.getExternalStorageDirectory().toString())) {
int permission = ActivityCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
requestPermission();

View File

@ -1,9 +1,9 @@
package de.danoeh.antennapod.activity;
import de.danoeh.antennapod.core.opml.OpmlElement;
import java.util.ArrayList;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
/**
* Hold infos gathered by Ompl-Import
* <p/>

View File

@ -26,11 +26,9 @@ import de.danoeh.antennapod.preferences.PreferenceController;
*/
public class PreferenceActivity extends AppCompatActivity {
private static WeakReference<PreferenceActivity> instance;
private PreferenceController preferenceController;
private MainFragment prefFragment;
private static WeakReference<PreferenceActivity> instance;
private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
@ -128,5 +126,14 @@ public class PreferenceActivity extends AppCompatActivity {
}
super.onPause();
}
@Override
public void onStop() {
PreferenceActivity activity = instance.get();
if(activity != null && activity.preferenceController != null) {
activity.preferenceController.onStop();
}
super.onStop();
}
}
}

View File

@ -18,9 +18,6 @@ import de.danoeh.antennapod.preferences.PreferenceController;
*/
public class PreferenceActivityGingerbread extends android.preference.PreferenceActivity {
private static final String TAG = "PreferenceActivity";
private PreferenceController preferenceController;
private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() {
@SuppressWarnings("deprecation")
@ -34,6 +31,7 @@ public class PreferenceActivityGingerbread extends android.preference.Preference
return PreferenceActivityGingerbread.this;
}
};
private PreferenceController preferenceController;
@SuppressLint("NewApi")
@SuppressWarnings("deprecation")
@ -60,6 +58,12 @@ public class PreferenceActivityGingerbread extends android.preference.Preference
super.onPause();
}
@Override
protected void onStop() {
preferenceController.onStop();
super.onStop();
}
@Override
protected void onApplyThemeResource(Theme theme, int resid, boolean first) {
theme.applyStyle(UserPreferences.getTheme(), true);

View File

@ -0,0 +1,23 @@
package de.danoeh.antennapod.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
/**
* Creator: vbarad
* Date: 2016-12-03
* Project: AntennaPod
*/
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}

View File

@ -47,6 +47,7 @@ public class ActionButtonUtils {
* Sets the displayed bitmap and content description of the given
* action button so that it matches the state of the FeedItem.
*/
@SuppressWarnings("ResourceType")
public void configureActionButton(ImageButton butSecondary, FeedItem item, boolean isInQueue) {
Validate.isTrue(butSecondary != null && item != null, "butSecondary or item was null");
@ -57,8 +58,7 @@ public class ActionButtonUtils {
if (isDownloadingMedia) {
// item is being downloaded
butSecondary.setVisibility(View.VISIBLE);
butSecondary.setImageDrawable(drawables
.getDrawable(1));
butSecondary.setImageDrawable(drawables.getDrawable(1));
butSecondary.setContentDescription(context.getString(labels[1]));
} else {
// item is not downloaded and not being downloaded

View File

@ -15,7 +15,7 @@ import de.danoeh.antennapod.core.util.NetworkUtils;
/**
* Utility methods for adapters
*/
public class AdapterUtils {
class AdapterUtils {
private static final String TAG = AdapterUtils.class.getSimpleName();
@ -26,7 +26,7 @@ public class AdapterUtils {
/**
* Updates the contents of the TextView that shows the current playback position and the ProgressBar.
*/
public static void updateEpisodePlaybackProgress(FeedItem item, TextView txtvPos, ProgressBar episodeProgress) {
static void updateEpisodePlaybackProgress(FeedItem item, TextView txtvPos, ProgressBar episodeProgress) {
FeedMedia media = item.getMedia();
episodeProgress.setVisibility(View.GONE);
if (media == null) {
@ -47,7 +47,6 @@ public class AdapterUtils {
- media.getPosition()));
}
} else if (!media.isDownloaded()) {
Log.d(TAG, "size: " + media.getSize());
if (media.getSize() > 0) {
txtvPos.setText(Converter.byteToString(media.getSize()));
} else if(NetworkUtils.isDownloadAllowed() && !media.checkedOnSizeButUnknown()) {

View File

@ -87,6 +87,7 @@ public class FeedItemlistAdapter extends BaseAdapter {
}
@Override
@SuppressWarnings("ResourceType")
public View getView(final int position, View convertView, ViewGroup parent) {
Holder holder;
final FeedItem item = getItem(position);

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.adapter.itunes;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
@ -18,6 +19,7 @@ import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.mfietz.fyydlin.SearchHit;
public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
/**
@ -42,8 +44,9 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
this.context = context;
}
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
//Current podcast
Podcast podcast = data.get(position);
@ -86,35 +89,6 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
return view;
}
/**
* View holder object for the GridView
*/
class PodcastViewHolder {
/**
* ImageView holding the Podcast image
*/
public final ImageView coverView;
/**
* TextView holding the Podcast title
*/
public final TextView titleView;
public final TextView urlView;
/**
* Constructor
* @param view GridView cell
*/
PodcastViewHolder(View view){
coverView = (ImageView) view.findViewById(R.id.imgvCover);
titleView = (TextView) view.findViewById(R.id.txtvTitle);
urlView = (TextView) view.findViewById(R.id.txtvUrl);
}
}
/**
* Represents an individual podcast on the iTunes Store.
*/
@ -154,6 +128,10 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
return new Podcast(title, imageUrl, feedUrl);
}
public static Podcast fromSearch(SearchHit searchHit) {
return new Podcast(searchHit.getTitle(), searchHit.getImageUrl(), searchHit.getXmlUrl());
}
/**
* Constructs a Podcast instance from iTunes toplist entry
*
@ -177,4 +155,33 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> {
}
}
/**
* View holder object for the GridView
*/
class PodcastViewHolder {
/**
* ImageView holding the Podcast image
*/
final ImageView coverView;
/**
* TextView holding the Podcast title
*/
final TextView titleView;
final TextView urlView;
/**
* Constructor
* @param view GridView cell
*/
PodcastViewHolder(View view){
coverView = (ImageView) view.findViewById(R.id.imgvCover);
titleView = (TextView) view.findViewById(R.id.txtvTitle);
urlView = (TextView) view.findViewById(R.id.txtvUrl);
}
}
}

View File

@ -0,0 +1,65 @@
package de.danoeh.antennapod.asynctask;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.LangUtils;
import rx.Observable;
/**
* Writes an OPML file into the export directory in the background.
*/
public class ExportWorker {
private static final String EXPORT_DIR = "export/";
private static final String TAG = "ExportWorker";
private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds";
private ExportWriter exportWriter;
private File output;
public ExportWorker(ExportWriter exportWriter) {
this(exportWriter, new File(UserPreferences.getDataFolder(EXPORT_DIR),
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()));
}
public ExportWorker(ExportWriter exportWriter, @NonNull File output) {
this.exportWriter = exportWriter;
this.output = output;
}
public Observable<File> exportObservable() {
if (output.exists()) {
Log.w(TAG, "Overwriting previously exported file.");
output.delete();
}
return Observable.create(subscriber -> {
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8);
exportWriter.writeDocument(DBReader.getFeedList(), writer);
subscriber.onNext(output);
} catch (IOException e) {
subscriber.onError(e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
subscriber.onError(e);
}
}
subscriber.onCompleted();
}
});
}
}

View File

@ -1,122 +0,0 @@
package de.danoeh.antennapod.asynctask;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.opml.OpmlWriter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.LangUtils;
/**
* Writes an OPML file into the export directory in the background.
*/
public class OpmlExportWorker extends AsyncTask<Void, Void, Void> {
private static final String TAG = "OpmlExportWorker";
private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds.opml";
public static final String EXPORT_DIR = "export/";
private Context context;
private File output;
private ProgressDialog progDialog;
private Exception exception;
public OpmlExportWorker(Context context, File output) {
this.context = context;
this.output = output;
}
public OpmlExportWorker(Context context) {
this.context = context;
}
@Override
protected Void doInBackground(Void... params) {
OpmlWriter opmlWriter = new OpmlWriter();
if (output == null) {
output = new File(
UserPreferences.getDataFolder(EXPORT_DIR),
DEFAULT_OUTPUT_NAME);
if (output.exists()) {
Log.w(TAG, "Overwriting previously exported file.");
output.delete();
}
}
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(output), LangUtils.UTF_8);
opmlWriter.writeDocument(DBReader.getFeedList(), writer);
} catch (IOException e) {
e.printStackTrace();
exception = e;
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ioe) {
exception = ioe;
}
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
progDialog.dismiss();
AlertDialog.Builder alert = new AlertDialog.Builder(context)
.setNeutralButton(android.R.string.ok,
(dialog, which) -> dialog.dismiss());
if (exception != null) {
alert.setTitle(R.string.export_error_label);
alert.setMessage(exception.getMessage());
} else {
alert.setTitle(R.string.opml_export_success_title);
alert.setMessage(context
.getString(R.string.opml_export_success_sum)
+ output.toString())
.setPositiveButton(R.string.send_label, (dialog, which) -> {
Uri outputUri = Uri.fromFile(output);
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_SUBJECT,
context.getResources().getText(R.string.opml_export_label));
sendIntent.putExtra(Intent.EXTRA_STREAM, outputUri);
sendIntent.setType("text/plain");
context.startActivity(Intent.createChooser(sendIntent,
context.getResources().getText(R.string.send_label)));
});
}
alert.create().show();
}
@Override
protected void onPreExecute() {
progDialog = new ProgressDialog(context);
progDialog.setMessage(context.getString(R.string.exporting_label));
progDialog.setIndeterminate(true);
progDialog.show();
}
@SuppressLint("NewApi")
public void executeAsync() {
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
executeOnExecutor(THREAD_POOL_EXECUTOR);
} else {
execute();
}
}
}

View File

@ -9,8 +9,8 @@ import java.util.Arrays;
import de.danoeh.antennapod.activity.OpmlImportHolder;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.opml.OpmlElement;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;

View File

@ -14,8 +14,8 @@ import java.io.Reader;
import java.util.ArrayList;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.opml.OpmlElement;
import de.danoeh.antennapod.core.opml.OpmlReader;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlReader;
public class OpmlImportWorker extends
AsyncTask<Void, Void, ArrayList<OpmlElement>> {

View File

@ -225,6 +225,10 @@ public class EpisodesApplyActionFragment extends Fragment {
checkQueued(false);
resId = R.string.selected_not_queued_label;
break;
case R.id.check_has_media:
checkWithMedia();
resId = R.string.selected_has_media_label;
break;
case R.id.sort_title_a_z:
sortByTitle(false);
return true;
@ -357,6 +361,17 @@ public class EpisodesApplyActionFragment extends Fragment {
refreshCheckboxes();
}
private void checkWithMedia() {
for (FeedItem episode : episodes) {
if(episode.hasMedia()) {
checkedIds.add(episode.getId());
} else {
checkedIds.remove(episode.getId());
}
}
refreshCheckboxes();
}
private void refreshTitles() {
titles.clear();
for(FeedItem episode : episodes) {

View File

@ -18,10 +18,6 @@ import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.internal.MDButton;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.net.InetSocketAddress;
@ -33,6 +29,10 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
@ -258,10 +258,11 @@ public class ProxyDialog {
SocketAddress address = InetSocketAddress.createUnresolved(host, portValue);
Proxy.Type proxyType = Proxy.Type.valueOf(type.toUpperCase());
Proxy proxy = new Proxy(proxyType, address);
OkHttpClient client = AntennapodHttpClient.newHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
client.setProxy(proxy);
client.interceptors().clear();
OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.proxy(proxy);
builder.interceptors().clear();
OkHttpClient client = builder.build();
if(!TextUtils.isEmpty(username)) {
String credentials = Credentials.basic(username, password);
client.interceptors().add(chain -> {

View File

@ -0,0 +1,44 @@
package de.danoeh.antennapod.dialog;
import android.app.Activity;
import android.text.InputType;
import com.afollestad.materialdialogs.MaterialDialog;
import java.lang.ref.WeakReference;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.storage.DBWriter;
public class RenameFeedDialog {
private final WeakReference<Activity> activityRef;
private final Feed feed;
public RenameFeedDialog(Activity activity, Feed feed) {
this.activityRef = new WeakReference<>(activity);
this.feed = feed;
}
public void show() {
Activity activity = activityRef.get();
if(activity == null) {
return;
}
new MaterialDialog.Builder(activity)
.title(de.danoeh.antennapod.core.R.string.rename_feed_label)
.inputType(InputType.TYPE_CLASS_TEXT)
.input(feed.getTitle(), feed.getTitle(), true, (dialog, input) -> {
feed.setCustomTitle(input.toString());
DBWriter.setFeedCustomTitle(feed);
dialog.dismiss();
})
.neutralText(de.danoeh.antennapod.core.R.string.reset)
.onNeutral((dialog, which) -> dialog.getInputEditText().setText(feed.getFeedTitle()))
.negativeText(de.danoeh.antennapod.core.R.string.cancel_label)
.onNegative((dialog, which) -> dialog.dismiss())
.autoDismiss(false)
.show();
}
}

View File

@ -1,7 +1,7 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.design.widget.Snackbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
@ -9,6 +9,7 @@ import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
@ -16,36 +17,27 @@ import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.greenrobot.event.EventBus;
public abstract class SleepTimerDialog {
private static final String TAG = SleepTimerDialog.class.getSimpleName();
private static final int DEFAULT_SPINNER_POSITION = 1;
private Context context;
private String PREF_NAME = "SleepTimerDialog";
private String PREF_VALUE = "LastValue";
private String PREF_TIME_UNIT = "LastTimeUnit";
private String PREF_VIBRATE = "Vibrate";
private String PREF_SHAKE_TO_RESET = "ShakeToReset";
private SharedPreferences prefs;
private MaterialDialog dialog;
private EditText etxtTime;
private Spinner spTimeUnit;
private CheckBox cbShakeToReset;
private CheckBox cbVibrate;
private CheckBox chAutoEnable;
private TimeUnit[] units = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS };
public SleepTimerDialog(Context context) {
protected SleepTimerDialog(Context context) {
this.context = context;
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public MaterialDialog createNewDialog() {
@ -58,7 +50,7 @@ public abstract class SleepTimerDialog {
builder.onPositive((dialog, which) -> {
try {
savePreferences();
long input = readTimeMillis();
long input = SleepTimerPreferences.timerMillis();
onTimerSet(input, cbShakeToReset.isChecked(), cbVibrate.isChecked());
dialog.dismiss();
} catch (NumberFormatException e) {
@ -75,8 +67,9 @@ public abstract class SleepTimerDialog {
spTimeUnit = (Spinner) view.findViewById(R.id.spTimeUnit);
cbShakeToReset = (CheckBox) view.findViewById(R.id.cbShakeToReset);
cbVibrate = (CheckBox) view.findViewById(R.id.cbVibrate);
chAutoEnable = (CheckBox) view.findViewById(R.id.chAutoEnable);
etxtTime.setText(prefs.getString(PREF_VALUE, "15"));
etxtTime.setText(SleepTimerPreferences.lastTimerValue());
etxtTime.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
@ -104,12 +97,17 @@ public abstract class SleepTimerDialog {
android.R.layout.simple_spinner_item, spinnerContent);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spTimeUnit.setAdapter(spinnerAdapter);
int selection = prefs.getInt(PREF_TIME_UNIT, DEFAULT_SPINNER_POSITION);
spTimeUnit.setSelection(selection);
spTimeUnit.setSelection(SleepTimerPreferences.lastTimerTimeUnit());
cbShakeToReset.setChecked(prefs.getBoolean(PREF_SHAKE_TO_RESET, true));
cbVibrate.setChecked(prefs.getBoolean(PREF_VIBRATE, true));
cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset());
cbVibrate.setChecked(SleepTimerPreferences.vibrate());
chAutoEnable.setChecked(SleepTimerPreferences.autoEnable());
chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked) -> {
SleepTimerPreferences.setAutoEnable(isChecked);
int messageString = isChecked ? R.string.sleep_timer_enabled_label : R.string.sleep_timer_disabled_label;
EventBus.getDefault().post(new MessageEvent(context.getString(messageString)));
});
return dialog;
}
@ -125,19 +123,12 @@ public abstract class SleepTimerDialog {
public abstract void onTimerSet(long millis, boolean shakeToReset, boolean vibrate);
private long readTimeMillis() {
TimeUnit selectedUnit = units[spTimeUnit.getSelectedItemPosition()];
long value = Long.parseLong(etxtTime.getText().toString());
return selectedUnit.toMillis(value);
}
private void savePreferences() {
prefs.edit()
.putString(PREF_VALUE, etxtTime.getText().toString())
.putInt(PREF_TIME_UNIT, spTimeUnit.getSelectedItemPosition())
.putBoolean(PREF_SHAKE_TO_RESET, cbShakeToReset.isChecked())
.putBoolean(PREF_VIBRATE, cbVibrate.isChecked())
.apply();
SleepTimerPreferences.setLastTimer(etxtTime.getText().toString(),
spTimeUnit.getSelectedItemPosition());
SleepTimerPreferences.setShakeToReset(cbShakeToReset.isChecked());
SleepTimerPreferences.setVibrate(cbVibrate.isChecked());
SleepTimerPreferences.setAutoEnable(chAutoEnable.isChecked());
}
}

View File

@ -39,10 +39,11 @@ public class AddFeedFragment extends Fragment {
etxtFeedurl.setText(args.getString(ARG_FEED_URL));
}
Button butSearchITunes = (Button) root.findViewById(R.id.butSearchItunes);
Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet);
Button butSearchFyyd = (Button) root.findViewById(R.id.butSearchFyyd);
Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport);
Button butConfirm = (Button) root.findViewById(R.id.butConfirm);
Button butSearchITunes = (Button) root.findViewById(R.id.butSearchItunes);
final MainActivity activity = (MainActivity) getActivity();
activity.getSupportActionBar().setTitle(R.string.add_feed_label);
@ -51,6 +52,8 @@ public class AddFeedFragment extends Fragment {
butBrowserGpoddernet.setOnClickListener(v -> activity.loadChildFragment(new GpodnetMainFragment()));
butSearchFyyd.setOnClickListener(v -> activity.loadChildFragment(new FyydSearchFragment()));
butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(),
OpmlImportFromPathActivity.class)));

View File

@ -227,22 +227,32 @@ public class AllEpisodesFragment extends Fragment {
}
return true;
case R.id.mark_all_read_item:
ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(),
ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.mark_all_read_label,
R.string.mark_all_read_confirmation_msg) {
@Override
public void onConfirmButtonPressed(
DialogInterface dialog) {
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.markAllItemsRead();
Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show();
}
};
conDialog.createNewDialog().show();
markAllReadConfirmationDialog.createNewDialog().show();
return true;
case R.id.mark_all_seen_item:
DBWriter.markNewItemsSeen();
ConfirmationDialog markAllSeenConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.mark_all_seen_label,
R.string.mark_all_seen_confirmation_msg) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.markNewItemsSeen();
Toast.makeText(getActivity(), R.string.mark_all_seen_msg, Toast.LENGTH_SHORT).show();
}
};
markAllSeenConfirmationDialog.createNewDialog().show();
return true;
default:
return false;

View File

@ -69,16 +69,18 @@ public class ChaptersFragment extends ListFragment implements MediaplayerInfoCon
@Override
public void onMediaChanged(Playable media) {
if(this.media == media || adapter == null) {
if(this.media == media) {
return;
}
this.media = media;
adapter.setMedia(media);
adapter.notifyDataSetChanged();
if(media == null || media.getChapters() == null || media.getChapters().size() == 0) {
setEmptyText(getString(R.string.no_items_label));
} else {
setEmptyText(null);
if (adapter != null) {
adapter.setMedia(media);
adapter.notifyDataSetChanged();
if(media == null || media.getChapters() == null || media.getChapters().size() == 0) {
setEmptyText(getString(R.string.no_items_label));
} else {
setEmptyText(null);
}
}
}

View File

@ -33,22 +33,15 @@ public class CoverFragment extends Fragment implements MediaplayerInfoContentFra
public static CoverFragment newInstance(Playable item) {
CoverFragment f = new CoverFragment();
if (item != null) {
Bundle args = new Bundle();
args.putParcelable(ARG_PLAYABLE, item);
f.setArguments(args);
}
f.media = item;
return f;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null) {
media = args.getParcelable(ARG_PLAYABLE);
} else {
Log.e(TAG, TAG + " was called with invalid arguments");
if (media == null) {
Log.e(TAG, TAG + " was called without media");
}
}
@ -98,11 +91,13 @@ public class CoverFragment extends Fragment implements MediaplayerInfoContentFra
@Override
public void onMediaChanged(Playable media) {
if(!isAdded() || this.media == media) {
if(this.media == media) {
return;
}
this.media = media;
loadMediaInfo();
if (isAdded()) {
loadMediaInfo();
}
}
}

View File

@ -0,0 +1,192 @@
package de.danoeh.antennapod.fragment;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.SearchView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.mfietz.fyydlin.FyydClient;
import de.mfietz.fyydlin.FyydResponse;
import de.mfietz.fyydlin.SearchHit;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast;
import static java.util.Collections.emptyList;
public class FyydSearchFragment extends Fragment {
private static final String TAG = "FyydSearchFragment";
/**
* Adapter responsible with the search results
*/
private ItunesAdapter adapter;
private GridView gridView;
private ProgressBar progressBar;
private TextView txtvError;
private Button butRetry;
private TextView txtvEmpty;
private FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient());
/**
* List of podcasts retreived from the search
*/
private List<Podcast> searchResults;
private Subscription subscription;
/**
* Constructor
*/
public FyydSearchFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View root = inflater.inflate(R.layout.fragment_itunes_search, container, false);
gridView = (GridView) root.findViewById(R.id.gridView);
adapter = new ItunesAdapter(getActivity(), new ArrayList<>());
gridView.setAdapter(adapter);
//Show information about the podcast when the list item is clicked
gridView.setOnItemClickListener((parent, view1, position, id) -> {
Podcast podcast = searchResults.get(position);
Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class);
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl);
intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title);
startActivity(intent);
});
progressBar = (ProgressBar) root.findViewById(R.id.progressBar);
txtvError = (TextView) root.findViewById(R.id.txtvError);
butRetry = (Button) root.findViewById(R.id.butRetry);
txtvEmpty = (TextView) root.findViewById(android.R.id.empty);
return root;
}
@Override
public void onDestroy() {
super.onDestroy();
if (subscription != null) {
subscription.unsubscribe();
}
adapter = null;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.itunes_search, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem);
MenuItemUtils.adjustTextColor(getActivity(), sv);
sv.setQueryHint(getString(R.string.search_fyyd_label));
sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
sv.clearFocus();
search(s);
return true;
}
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
getActivity().getSupportFragmentManager().popBackStack();
return true;
}
});
MenuItemCompat.expandActionView(searchItem);
}
private void search(String query) {
if (subscription != null) {
subscription.unsubscribe();
}
showOnlyProgressBar();
subscription = client.searchPodcasts(query)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
progressBar.setVisibility(View.GONE);
processSearchResult(result);
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
progressBar.setVisibility(View.GONE);
txtvError.setText(error.toString());
txtvError.setVisibility(View.VISIBLE);
butRetry.setOnClickListener(v -> search(query));
butRetry.setVisibility(View.VISIBLE);
});
}
private void showOnlyProgressBar() {
gridView.setVisibility(View.GONE);
txtvError.setVisibility(View.GONE);
butRetry.setVisibility(View.GONE);
txtvEmpty.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
void processSearchResult(FyydResponse response) {
adapter.clear();
if (!response.getData().isEmpty()) {
adapter.clear();
searchResults = new ArrayList<>();
for (SearchHit searchHit : response.getData().values()) {
Podcast podcast = Podcast.fromSearch(searchHit);
searchResults.add(podcast);
}
} else {
searchResults = emptyList();
}
for(Podcast podcast : searchResults) {
adapter.add(podcast);
}
adapter.notifyDataSetInvalidated();
gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE);
txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE);
}
}

View File

@ -185,8 +185,10 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo
super.onViewCreated(view, savedInstanceState);
Bundle args = getArguments();
if (args.containsKey(ARG_PLAYABLE)) {
media = args.getParcelable(ARG_PLAYABLE);
shownotesProvider = media;
if (media == null) {
media = args.getParcelable(ARG_PLAYABLE);
shownotesProvider = media;
}
load();
} else if (args.containsKey(ARG_FEEDITEM_ID)) {
long id = getArguments().getLong(ARG_FEEDITEM_ID);
@ -377,12 +379,14 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo
@Override
public void onMediaChanged(Playable media) {
if(this.media == media || webvDescription == null) {
if(this.media == media) {
return;
}
this.media = media;
this.shownotesProvider = media;
load();
if (webvDescription != null) {
load();
}
}
}

View File

@ -67,6 +67,7 @@ import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
@ -99,6 +100,7 @@ public class ItemlistFragment extends ListFragment {
private boolean itemsLoaded = false;
private boolean viewsCreated = false;
private boolean headerCreated = false;
private List<Downloader> downloaderList;
@ -106,6 +108,7 @@ public class ItemlistFragment extends ListFragment {
private boolean isUpdatingFeed;
private TextView txtvTitle;
private IconTextView txtvFailure;
private TextView txtvInformation;
@ -248,6 +251,9 @@ public class ItemlistFragment extends ListFragment {
.newInstance(feed.getItems());
((MainActivity)getActivity()).loadChildFragment(fragment);
return true;
case R.id.rename_item:
new RenameFeedDialog(getActivity(), feed).show();
return true;
case R.id.remove_item:
final FeedRemover remover = new FeedRemover(
getActivity(), feed) {
@ -415,6 +421,7 @@ public class ItemlistFragment extends ListFragment {
public void update(EventDistributor eventDistributor, Integer arg) {
if ((EVENTS & arg) != 0) {
Log.d(TAG, "Received contentUpdate Intent. arg " + arg);
refreshHeaderView();
loadItems();
updateProgressBarVisibility();
}
@ -460,8 +467,8 @@ public class ItemlistFragment extends ListFragment {
}
private void refreshHeaderView() {
if (getListView() == null || feed == null) {
Log.e(TAG, "Unable to setup listview: recyclerView = null or feed = null");
if (getListView() == null || feed == null || !headerCreated) {
Log.e(TAG, "Unable to refresh header view");
return;
}
if(feed.hasLastUpdateFailed()) {
@ -469,6 +476,7 @@ public class ItemlistFragment extends ListFragment {
} else {
txtvFailure.setVisibility(View.GONE);
}
txtvTitle.setText(feed.getTitle());
if(feed.getItemFilter() != null) {
FeedItemFilter filter = feed.getItemFilter();
if(filter.getValues().length > 0) {
@ -498,7 +506,7 @@ public class ItemlistFragment extends ListFragment {
View header = inflater.inflate(R.layout.feeditemlist_header, lv, false);
lv.addHeaderView(header);
TextView txtvTitle = (TextView) header.findViewById(R.id.txtvTitle);
txtvTitle = (TextView) header.findViewById(R.id.txtvTitle);
TextView txtvAuthor = (TextView) header.findViewById(R.id.txtvAuthor);
ImageView imgvBackground = (ImageView) header.findViewById(R.id.imgvBackground);
ImageView imgvCover = (ImageView) header.findViewById(R.id.imgvCover);
@ -539,6 +547,7 @@ public class ItemlistFragment extends ListFragment {
startActivity(startIntent);
}
});
headerCreated = true;
}

View File

@ -18,9 +18,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import org.json.JSONArray;
import org.json.JSONException;
@ -39,6 +36,9 @@ import de.danoeh.antennapod.adapter.itunes.ItunesAdapter;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;

View File

@ -26,6 +26,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
@ -102,7 +103,7 @@ public class SubscriptionFragment extends Fragment {
if(subscription != null) {
subscription.unsubscribe();
}
subscription = Observable.fromCallable(() -> DBReader.getNavDrawerData())
subscription = Observable.fromCallable(DBReader::getNavDrawerData)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
@ -164,6 +165,9 @@ public class SubscriptionFragment extends Fragment {
.subscribe(result -> loadSubscriptions(),
error -> Log.e(TAG, Log.getStackTraceString(error)));
return true;
case R.id.rename_item:
new RenameFeedDialog(getActivity(), feed).show();
return true;
case R.id.remove_item:
final FeedRemover remover = new FeedRemover(getContext(), feed) {
@Override

View File

@ -28,6 +28,7 @@ public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuIte
}
}
@SuppressWarnings("ResourceType")
public static void refreshLockItem(Context context, Menu menu) {
final MenuItem queueLock = menu.findItem(de.danoeh.antennapod.R.id.queue_lock);
int[] lockIcons = new int[] { de.danoeh.antennapod.R.attr.ic_lock_open, de.danoeh.antennapod.R.attr.ic_lock_closed };

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.preferences;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.app.TimePickerDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
@ -42,6 +43,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -55,7 +58,10 @@ import de.danoeh.antennapod.activity.MediaplayerActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.activity.PreferenceActivityGingerbread;
import de.danoeh.antennapod.activity.StatisticsActivity;
import de.danoeh.antennapod.asynctask.OpmlExportWorker;
import de.danoeh.antennapod.asynctask.ExportWorker;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.export.html.HtmlWriter;
import de.danoeh.antennapod.core.export.opml.OpmlWriter;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
@ -67,6 +73,10 @@ import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog;
import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
import de.danoeh.antennapod.dialog.ProxyDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**
* Sets up a preference UI that lets the user change user preferences.
@ -76,55 +86,36 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
private static final String TAG = "PreferenceController";
public static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings";
public static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate";
public static final String PREF_FLATTR_REVOKE = "prefRevokeAccess";
public static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs";
public static final String PREF_OPML_EXPORT = "prefOpmlExport";
public static final String STATISTICS = "statistics";
public static final String PREF_ABOUT = "prefAbout";
public static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
public static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings";
public static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher";
private static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings";
private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate";
private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess";
private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs";
private static final String PREF_OPML_EXPORT = "prefOpmlExport";
private static final String PREF_HTML_EXPORT = "prefHtmlExport";
private static final String STATISTICS = "statistics";
private static final String PREF_ABOUT = "prefAbout";
private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings";
private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher";
public static final String PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher";
public static final String PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher";
public static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
public static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
public static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync";
public static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
public static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname";
public static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications";
public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
public static final String PREF_PROXY = "prefProxy";
public static final String PREF_KNOWN_ISSUES = "prefKnownIssues";
public static final String PREF_FAQ = "prefFaq";
public static final String PREF_SEND_CRASH_REPORT = "prefSendCrashReport";
private final PreferenceUI ui;
private CheckBoxPreference[] selectedNetworks;
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";
private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications";
private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
private static final String PREF_PROXY = "prefProxy";
private static final String PREF_KNOWN_ISSUES = "prefKnownIssues";
private static final String PREF_FAQ = "prefFaq";
private static final String PREF_SEND_CRASH_REPORT = "prefSendCrashReport";
private static final String[] EXTERNAL_STORAGE_PERMISSIONS = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE };
private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41;
public PreferenceController(PreferenceUI ui) {
this.ui = ui;
PreferenceManager.getDefaultSharedPreferences(ui.getActivity().getApplicationContext())
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if(key.equals(UserPreferences.PREF_SONIC)) {
CheckBoxPreference prefSonic = (CheckBoxPreference) ui.findPreference(UserPreferences.PREF_SONIC);
if(prefSonic != null) {
prefSonic.setChecked(sharedPreferences.getBoolean(UserPreferences.PREF_SONIC, false));
}
}
}
private final PreferenceUI ui;
private final SharedPreferences.OnSharedPreferenceChangeListener gpoddernetListener =
(sharedPreferences, key) -> {
if (GpodnetPreferences.PREF_LAST_SYNC_ATTEMPT_TIMESTAMP.equals(key)) {
@ -132,6 +123,14 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
GpodnetPreferences.getLastSyncAttemptTimestamp());
}
};
private CheckBoxPreference[] selectedNetworks;
private Subscription subscription;
public PreferenceController(PreferenceUI ui) {
this.ui = ui;
PreferenceManager.getDefaultSharedPreferences(ui.getActivity().getApplicationContext())
.registerOnSharedPreferenceChangeListener(this);
}
/**
* Returns the preference activity that should be used on this device.
@ -146,6 +145,16 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if(key.equals(UserPreferences.PREF_SONIC)) {
CheckBoxPreference prefSonic = (CheckBoxPreference) ui.findPreference(UserPreferences.PREF_SONIC);
if(prefSonic != null) {
prefSonic.setChecked(sharedPreferences.getBoolean(UserPreferences.PREF_SONIC, false));
}
}
}
public void onCreate() {
final Activity activity = ui.getActivity();
@ -181,11 +190,9 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
);
ui.findPreference(PreferenceController.PREF_OPML_EXPORT).setOnPreferenceClickListener(
preference -> {
new OpmlExportWorker(activity).executeAsync();
return true;
}
);
preference -> export(new OpmlWriter()));
ui.findPreference(PreferenceController.PREF_HTML_EXPORT).setOnPreferenceClickListener(
preference -> export(new HtmlWriter()));
ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(
preference -> {
if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT &&
@ -361,6 +368,18 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
toast.show();
return true;
});
ui.findPreference(PreferenceController.PREF_GPODNET_FORCE_FULL_SYNC).
setOnPreferenceClickListener(preference -> {
GpodnetPreferences.setLastSubscriptionSyncTimestamp(0L);
GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(0L);
GpodnetPreferences.setLastSyncAttempt(false, 0);
updateLastGpodnetSyncReport(false, 0);
GpodnetSyncService.sendSyncIntent(ui.getActivity().getApplicationContext());
Toast toast = Toast.makeText(ui.getActivity(), R.string.pref_gpodnet_sync_started,
Toast.LENGTH_SHORT);
toast.show();
return true;
});
ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(
preference -> {
GpodnetPreferences.logout();
@ -440,6 +459,40 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter());
}
private boolean export(ExportWriter exportWriter) {
Context context = ui.getActivity();
final ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setMessage(context.getString(R.string.exporting_label));
progressDialog.setIndeterminate(true);
progressDialog.show();
final AlertDialog.Builder alert = new AlertDialog.Builder(context)
.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
Observable<File> observable = new ExportWorker(exportWriter).exportObservable();
subscription = observable.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(output -> {
alert.setTitle(R.string.opml_export_success_title);
String message = context.getString(R.string.opml_export_success_sum) + output.toString();
alert.setMessage(message);
alert.setPositiveButton(R.string.send_label, (dialog, which) -> {
Uri outputUri = Uri.fromFile(output);
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_SUBJECT,
context.getResources().getText(R.string.opml_export_label));
sendIntent.putExtra(Intent.EXTRA_STREAM, outputUri);
sendIntent.setType("text/plain");
context.startActivity(Intent.createChooser(sendIntent,
context.getResources().getText(R.string.send_label)));
});
alert.create().show();
}, error -> {
alert.setTitle(R.string.export_error_label);
alert.setMessage(error.getMessage());
alert.show();
}, () -> progressDialog.dismiss());
return true;
}
private void openInBrowser(String url) {
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@ -464,6 +517,12 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
GpodnetPreferences.unregisterOnSharedPreferenceChangeListener(gpoddernetListener);
}
public void onStop() {
if(subscription != null) {
subscription.unsubscribe();
}
}
@SuppressLint("NewApi")
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK &&
@ -505,6 +564,7 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
ui.findPreference(PreferenceController.PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
ui.findPreference(PreferenceController.PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn);
ui.findPreference(PreferenceController.PREF_GPODNET_SYNC).setEnabled(loggedIn);
ui.findPreference(PreferenceController.PREF_GPODNET_FORCE_FULL_SYNC).setEnabled(loggedIn);
ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
ui.findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(loggedIn);
if(loggedIn) {
@ -524,7 +584,7 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
private void updateLastGpodnetSyncReport(boolean successful, long lastTime) {
Preference sync = ui.findPreference(PREF_GPODNET_SYNC);
if (lastTime != 0) {
sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_sum) + "\n" +
sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_changes_sum) + "\n" +
ui.getActivity().getString(R.string.pref_gpodnet_sync_sum_last_sync_line,
ui.getActivity().getString(successful ?
R.string.gpodnetsync_pref_report_successful :
@ -535,7 +595,7 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_SHOW_TIME)));
} else {
sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_sum));
sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_changes_sum));
}
}
@ -703,6 +763,12 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks();
if (networks != null) {
Collections.sort(networks, new Comparator<WifiConfiguration>() {
@Override
public int compare(WifiConfiguration x, WifiConfiguration y) {
return x.SSID.compareTo(y.SSID);
}
});
selectedNetworks = new CheckBoxPreference[networks.size()];
List<String> prefValues = Arrays.asList(UserPreferences
.getAutodownloadSelectedNetworks());
@ -787,9 +853,8 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]);
}
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) ->
UserPreferences.setHiddenDrawerItems(hiddenDrawerItems));
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
}
@ -833,9 +898,8 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
preferredButtons.remove((Integer) which);
}
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
UserPreferences.setCompactNotificationButtons(preferredButtons);
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) ->
UserPreferences.setCompactNotificationButtons(preferredButtons));
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
}

View File

@ -0,0 +1,43 @@
AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the iTunes podcast database, OPML files or simple RSS URLs. Save effort, battery power and mobile data usage with powerful automation controls for downloading episodes (specify times, intervals and WiFi networks) and deleting episodes (based your favourites and delay settings).<br>
But most importantly: Download, stream or queue episodes and enjoy them the way you like with adjustable playback speeds, chapter support and a sleep timer. You can even show your love to the content creators with our Flattr integration.
Made by podcast-enthousiast, AntennaPod is free in all senses of the word: open source, no costs, no ads.
<b>All features:</b><br>
IMPORT, ORGANIZE AND PLAY<br>
&#8226; Add and import feeds via the iTunes and gPodder.net directories, OPML files and RSS or Atom links<br>
&#8226; Manage playback from anywhere: homescreen widget, system notification and earplug and bluetooth controls<br>
&#8226; Enjoy listening your way with adjustable playback speed, chapter support (MP3, VorbisComment and Podlove), remembered playback position and an advanced sleep timer (shake to reset, lower volume and slow down playback)<br>
&#8226; Access password-protected feeds and episodes<br>
&#8226; Take advantage of paged feeds (www.podlove.org/paged-feeds)
KEEP TRACK, SHARE & APPRECIATE<br>
&#8226; Keep track of the best of the best by marking episodes as favourites<br>
&#8226; Find that one episode through the playback history or by searching (titles and shownotes)<br>
&#8226; Share episodes and feeds through advanced social media and email options, the gPodder.net services and via OPML export<br>
&#8226; Support content creators with Flattr integration including automatic flattring
CONTROL THE SYSTEM<br>
&#8226; Take control over automated downloading: choose feeds, exclude mobile networks, select specific WiFi networks, require the phone to be charging and set times or intervals<br>
&#8226; Manage storage by setting the amount of cached episodes, smart deletion (based on your favourites and play status) and selecting your preferred location<br>
&#8226; Use AntennaPod in your language (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)<br>
&#8226; Adapt to your environment using the light and dark theme<br>
&#8226; Back-up your subscriptions with the gPodder.net integration and OPML export
<b>Join the AntennaPod community!</b><br>
AntennaPod is under active development by volunteers. You can contribute too, with code or with comment!
GitHub is the place to go for feature requests, bug reports and code contributions:<br>
https://www.github.com/AntennaPod/AntennaPod
Our Google Group is the place to share your ideas, favourite podcasting moments and gratitude to all the volunteers:<br>
https://groups.google.com/forum/#!forum/antennapod
Have a question or want to give us feedback?
https://twitter.com/@AntennaPod
Transifex is the place to help with translations:<br>
https://www.transifex.com/antennapod/antennapod
Check out our Beta Testing programme to get the latest features first:<br>
https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod

View File

@ -0,0 +1,43 @@
AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the iTunes podcast database, OPML files or simple RSS URLs. Save effort, battery power and mobile data usage with powerful automation controls for downloading episodes (specify times, intervals and WiFi networks) and deleting episodes (based your favourites and delay settings).<br>
But most importantly: Download, stream or queue episodes and enjoy them the way you like with adjustable playback speeds, chapter support and a sleep timer. You can even show your love to the content creators with our Flattr integration.
Made by podcast-enthousiast, AntennaPod is free in all senses of the word: open source, no costs, no ads.
<b>All features:</b><br>
IMPORT, ORGANIZE AND PLAY<br>
&#8226; Add and import feeds via the iTunes and gPodder.net directories, OPML files and RSS or Atom links<br>
&#8226; Manage playback from anywhere: homescreen widget, system notification and earplug and bluetooth controls<br>
&#8226; Enjoy listening your way with adjustable playback speed, chapter support (MP3, VorbisComment and Podlove), remembered playback position and an advanced sleep timer (shake to reset, lower volume and slow down playback)<br>
&#8226; Access password-protected feeds and episodes<br>
&#8226; Take advantage of paged feeds (www.podlove.org/paged-feeds)
KEEP TRACK, SHARE & APPRECIATE<br>
&#8226; Keep track of the best of the best by marking episodes as favourites<br>
&#8226; Find that one episode through the playback history or by searching (titles and shownotes)<br>
&#8226; Share episodes and feeds through advanced social media and email options, the gPodder.net services and via OPML export<br>
&#8226; Support content creators with Flattr integration including automatic flattring
CONTROL THE SYSTEM<br>
&#8226; Take control over automated downloading: choose feeds, exclude mobile networks, select specific WiFi networks, require the phone to be charging and set times or intervals<br>
&#8226; Manage storage by setting the amount of cached episodes, smart deletion (based on your favourites and play status) and selecting your preferred location<br>
&#8226; Use AntennaPod in your language (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)<br>
&#8226; Adapt to your environment using the light and dark theme<br>
&#8226; Back-up your subscriptions with the gPodder.net integration and OPML export
<b>Join the AntennaPod community!</b><br>
AntennaPod is under active development by volunteers. You can contribute too, with code or with comment!
GitHub is the place to go for feature requests, bug reports and code contributions:<br>
https://www.github.com/AntennaPod/AntennaPod
Our Google Group is the place to share your ideas, favourite podcasting moments and gratitude to all the volunteers:<br>
https://groups.google.com/forum/#!forum/antennapod
Have a question or want to give us feedback?
https://twitter.com/@AntennaPod
Transifex is the place to help with translations:<br>
https://www.transifex.com/antennapod/antennapod
Check out our Beta Testing programme to get the latest features first:<br>
https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod

View File

@ -0,0 +1,43 @@
AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the iTunes podcast database, OPML files or simple RSS URLs. Save effort, battery power and mobile data usage with powerful automation controls for downloading episodes (specify times, intervals and WiFi networks) and deleting episodes (based your favourites and delay settings).<br>
But most importantly: Download, stream or queue episodes and enjoy them the way you like with adjustable playback speeds, chapter support and a sleep timer. You can even show your love to the content creators with our Flattr integration.
Made by podcast-enthousiast, AntennaPod is free in all senses of the word: open source, no costs, no ads.
<b>All features:</b><br>
IMPORT, ORGANIZE AND PLAY<br>
&#8226; Add and import feeds via the iTunes and gPodder.net directories, OPML files and RSS or Atom links<br>
&#8226; Manage playback from anywhere: homescreen widget, system notification and earplug and bluetooth controls<br>
&#8226; Enjoy listening your way with adjustable playback speed, chapter support (MP3, VorbisComment and Podlove), remembered playback position and an advanced sleep timer (shake to reset, lower volume and slow down playback)<br>
&#8226; Access password-protected feeds and episodes<br>
&#8226; Take advantage of paged feeds (www.podlove.org/paged-feeds)
KEEP TRACK, SHARE & APPRECIATE<br>
&#8226; Keep track of the best of the best by marking episodes as favourites<br>
&#8226; Find that one episode through the playback history or by searching (titles and shownotes)<br>
&#8226; Share episodes and feeds through advanced social media and email options, the gPodder.net services and via OPML export<br>
&#8226; Support content creators with Flattr integration including automatic flattring
CONTROL THE SYSTEM<br>
&#8226; Take control over automated downloading: choose feeds, exclude mobile networks, select specific WiFi networks, require the phone to be charging and set times or intervals<br>
&#8226; Manage storage by setting the amount of cached episodes, smart deletion (based on your favourites and play status) and selecting your preferred location<br>
&#8226; Use AntennaPod in your language (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)<br>
&#8226; Adapt to your environment using the light and dark theme<br>
&#8226; Back-up your subscriptions with the gPodder.net integration and OPML export
<b>Join the AntennaPod community!</b><br>
AntennaPod is under active development by volunteers. You can contribute too, with code or with comment!
GitHub is the place to go for feature requests, bug reports and code contributions:<br>
https://www.github.com/AntennaPod/AntennaPod
Our Google Group is the place to share your ideas, favourite podcasting moments and gratitude to all the volunteers:<br>
https://groups.google.com/forum/#!forum/antennapod
Have a question or want to give us feedback?
https://twitter.com/@AntennaPod
Transifex is the place to help with translations:<br>
https://www.transifex.com/antennapod/antennapod
Check out our Beta Testing programme to get the latest features first:<br>
https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod

View File

@ -0,0 +1,43 @@
AntennaPod is a podcast manager and player that gives you instant access to millions of free and paid podcasts, from independent podcasters to large publishing houses such as the BBC, NPR and CNN. Add, import and export their feeds hassle-free using the iTunes podcast database, OPML files or simple RSS URLs. Save effort, battery power and mobile data usage with powerful automation controls for downloading episodes (specify times, intervals and WiFi networks) and deleting episodes (based your favourites and delay settings).<br>
But most importantly: Download, stream or queue episodes and enjoy them the way you like with adjustable playback speeds, chapter support and a sleep timer. You can even show your love to the content creators with our Flattr integration.
Made by podcast-enthousiast, AntennaPod is free in all senses of the word: open source, no costs, no ads.
<b>All features:</b><br>
IMPORT, ORGANIZE AND PLAY<br>
&#8226; Add and import feeds via the iTunes and gPodder.net directories, OPML files and RSS or Atom links<br>
&#8226; Manage playback from anywhere: homescreen widget, system notification and earplug and bluetooth controls<br>
&#8226; Enjoy listening your way with adjustable playback speed, chapter support (MP3, VorbisComment and Podlove), remembered playback position and an advanced sleep timer (shake to reset, lower volume and slow down playback)<br>
&#8226; Access password-protected feeds and episodes<br>
&#8226; Take advantage of paged feeds (www.podlove.org/paged-feeds)
KEEP TRACK, SHARE & APPRECIATE<br>
&#8226; Keep track of the best of the best by marking episodes as favourites<br>
&#8226; Find that one episode through the playback history or by searching (titles and shownotes)<br>
&#8226; Share episodes and feeds through advanced social media and email options, the gPodder.net services and via OPML export<br>
&#8226; Support content creators with Flattr integration including automatic flattring
CONTROL THE SYSTEM<br>
&#8226; Take control over automated downloading: choose feeds, exclude mobile networks, select specific WiFi networks, require the phone to be charging and set times or intervals<br>
&#8226; Manage storage by setting the amount of cached episodes, smart deletion (based on your favourites and play status) and selecting your preferred location<br>
&#8226; Use AntennaPod in your language (EN, DE, CS, NL, NB, JA, PT, ES, SV, CA, UK, FR, KO, TR, ZH)<br>
&#8226; Adapt to your environment using the light and dark theme<br>
&#8226; Back-up your subscriptions with the gPodder.net integration and OPML export
<b>Join the AntennaPod community!</b><br>
AntennaPod is under active development by volunteers. You can contribute too, with code or with comment!
GitHub is the place to go for feature requests, bug reports and code contributions:<br>
https://www.github.com/AntennaPod/AntennaPod
Our Google Group is the place to share your ideas, favourite podcasting moments and gratitude to all the volunteers:<br>
https://groups.google.com/forum/#!forum/antennapod
Have a question or want to give us feedback?
https://twitter.com/@AntennaPod
Transifex is the place to help with translations:<br>
https://www.transifex.com/antennapod/antennapod
Check out our Beta Testing programme to get the latest features first:<br>
https://www.github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod

View File

@ -33,26 +33,34 @@
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginTop="8dp"
android:text="@string/timer_about_to_expire_label"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@string/timer_about_to_expire_label"/>
<CheckBox
android:id="@+id/cbShakeToReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shake_to_reset_label" />
<CheckBox android:id="@+id/cbShakeToReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shake_to_reset_label"/>
<CheckBox
android:id="@+id/cbVibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/timer_vibration_label" />
<CheckBox android:id="@+id/cbVibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/timer_vibration_label"/>
<CheckBox
android:id="@+id/chAutoEnable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_enable_label" />
</LinearLayout>

View File

@ -7,18 +7,18 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:orientation="vertical">
android:paddingTop="8dp">
<TextView
android:id="@+id/txtvPodcastDirectories"
style="@style/AntennaPod.TextView.Heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/AntennaPod.TextView.Heading"
android:text="@string/podcastdirectories_label"/>
<TextView
@ -26,83 +26,73 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/podcastdirectories_descr"
android:textSize="@dimen/text_size_medium"
android:layout_marginTop="4dp"/>
android:textSize="@dimen/text_size_medium"/>
<Button
android:id="@+id/butSearchItunes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="4dp"
android:text="@string/search_itunes_label"/>
<Button
android:id="@+id/butSearchFyyd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/search_fyyd_label"/>
<Button
android:id="@+id/butBrowseGpoddernet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/browse_gpoddernet_label"/>
<View
android:id="@+id/divider1"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<View style="@style/Divider"/>
<TextView
android:id="@+id/txtvFeedurl"
style="@style/AntennaPod.TextView.Heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/divider1"
style="@style/AntennaPod.TextView.Heading"
android:text="@string/txtvfeedurl_label"/>
<EditText
android:id="@+id/etxtFeedurl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:hint="@string/etxtFeedurlHint"
android:inputType="textUri"
android:cursorVisible="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:cursorVisible="true"/>
android:hint="@string/etxtFeedurlHint"
android:inputType="textUri"/>
<Button
android:id="@+id/butConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/confirm_label"/>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="?android:attr/listDivider"/>
<View style="@style/Divider"/>
<TextView
android:id="@+id/txtvOpmlImport"
style="@style/AntennaPod.TextView.Heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/AntennaPod.TextView.Heading"
android:text="@string/opml_import_label"/>
<TextView
android:id="@+id/txtvOpmlImportExpl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="@dimen/text_size_medium"
android:text="@string/opml_import_txtv_button_lable"/>
android:text="@string/opml_import_txtv_button_lable"
android:textSize="@dimen/text_size_medium"/>
<Button
android:id="@+id/butOpmlImport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="4dp"
android:text="@string/opml_import_label"/>
</LinearLayout>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version='1.0' encoding='utf-8'?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_layout"
android:layout_width="@dimen/drawer_width"
@ -23,8 +21,6 @@
android:id="@+id/imgvCover"
android:layout_width="@dimen/thumbnail_length_navlist"
android:layout_height="@dimen/thumbnail_length_navlist"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginBottom="4dp"
android:layout_marginLeft="@dimen/listitem_icon_leftpadding"
android:layout_marginTop="4dp"
@ -34,8 +30,8 @@
android:padding="8dp"
android:scaleType="centerCrop"
android:src="?attr/ic_settings"
tools:src="@android:drawable/sym_def_app_icon"
tools:background="@android:color/holo_orange_dark" />
tools:background="@android:color/holo_orange_dark"
tools:src="@android:drawable/sym_def_app_icon"></ImageView>
<TextView
android:layout_width="wrap_content"
@ -47,16 +43,15 @@
android:text="@string/settings_label"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_navdrawer"
tools:background="@android:color/holo_green_light"/>
tools:background="@android:color/holo_green_light" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="@dimen/drawer_width"
android:layout_height="1dp"
android:layout_centerVertical="true"
android:layout_above="@id/nav_settings"
android:layout_centerVertical="true"
android:background="?android:attr/listDivider"
tools:background="@android:color/holo_red_dark" />
@ -64,7 +59,6 @@
android:id="@+id/nav_list"
android:layout_width="@dimen/drawer_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_above="@id/divider"
android:layout_alignParentTop="true"
android:choiceMode="singleChoice"
@ -74,7 +68,6 @@
android:paddingBottom="@dimen/list_vertical_padding"
android:paddingTop="@dimen/list_vertical_padding"
android:scrollbarStyle="outsideOverlay"
tools:listitem="@layout/nav_listitem"
tools:background="@android:color/holo_purple" />
tools:background="@android:color/holo_purple"
tools:listitem="@layout/nav_listitem"></ListView>
</RelativeLayout>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version='1.0' encoding='utf-8'?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
@ -10,31 +10,28 @@
android:id="@+id/imgvCover"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:scaleType="centerCrop"
tools:src="@drawable/ic_launcher" />
tools:src="@drawable/ic_launcher">
</de.danoeh.antennapod.view.SquareImageView>
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray"
android:ellipsize="end"
android:gravity="center"
android:background="@color/light_gray"
tools:text="@string/app_name" />
<jp.shts.android.library.TriangleLabelView
android:id="@+id/triangleCountView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="right|top"
app:backgroundColor="@color/antennapod_blue"
app:corner="rightTop"
app:primaryText="Test"
app:primaryTextColor="@color/white"
app:primaryTextSize="12sp"
android:layout_gravity="right|top"/>
app:primaryTextSize="12sp">
</jp.shts.android.library.TriangleLabelView>
</FrameLayout>

View File

@ -36,22 +36,30 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:text="@string/timer_about_to_expire_label"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/timer_about_to_expire_label"
android:textSize="16sp" />
<CheckBox android:id="@+id/cbShakeToReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shake_to_reset_label"/>
<CheckBox
android:id="@+id/cbShakeToReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shake_to_reset_label" />
<CheckBox android:id="@+id/cbVibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/timer_vibration_label"/>
<CheckBox
android:id="@+id/cbVibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/timer_vibration_label" />
<CheckBox
android:id="@+id/chAutoEnable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_enable_label" />
</LinearLayout>

View File

@ -46,6 +46,8 @@
android:title="@string/queued_label"/>
<item android:id="@+id/check_not_queued"
android:title="@string/not_queued_label"/>
<item android:id="@+id/check_has_media"
android:title="@string/has_media"/>
</menu>
</item>

View File

@ -65,6 +65,12 @@
android:title="@string/share_feed_url_label">
</item>
<item
android:id="@+id/rename_item"
android:menuCategory="container"
android:title="@string/rename_feed_label"
custom:showAsAction="never" />
<item
android:id="@+id/remove_item"
android:icon="?attr/content_discard"

View File

@ -11,6 +11,11 @@
android:menuCategory="container"
android:title="@string/mark_all_read_label" />
<item
android:id="@+id/rename_item"
android:menuCategory="container"
android:title="@string/rename_feed_label" />
<item
android:id="@+id/remove_item"
android:menuCategory="container"

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="media"/>
</automotiveApp>

View File

@ -117,6 +117,12 @@
android:key="prefPlaybackRewindDeltaLauncher"
android:summary="@string/pref_rewind_sum"
android:title="@string/pref_rewind" />
<de.danoeh.antennapod.preferences.SwitchCompatPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefHardwarePreviousButtonRestarts"
android:summary="@string/pref_hardwarePreviousButtonRestarts_sum"
android:title="@string/pref_hardwarePreviousButtonRestarts_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
android:defaultValue="true"
android:enabled="true"
@ -162,6 +168,12 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/network_pref">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefEnqueueDownloaded"
android:summary="@string/pref_enqueue_downloaded_summary"
android:title="@string/pref_enqueue_downloaded_title" />
<Preference
android:key="prefAutoUpdateIntervall"
android:summary="@string/pref_autoUpdateIntervallOrTime_sum"
@ -254,8 +266,12 @@
android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
<Preference
android:key="pref_gpodnet_sync"
android:title="@string/pref_gpodnet_sync_title"
android:summary="@string/pref_gpodnet_sync_sum"/>
android:title="@string/pref_gpodnet_sync_changes_title"
android:summary="@string/pref_gpodnet_sync_changes_sum"/>
<Preference
android:key="pref_gpodnet_force_full_sync"
android:title="@string/pref_gpodnet_full_sync_title"
android:summary="@string/pref_gpodnet_full_sync_sum"/>
<Preference
android:key="pref_gpodnet_logout"
android:title="@string/pref_gpodnet_logout_title"/>
@ -285,6 +301,9 @@
<Preference
android:key="prefOpmlExport"
android:title="@string/opml_export_label"/>
<Preference
android:key="prefHtmlExport"
android:title="@string/html_export_label"/>
<Preference
android:key="statistics"
android:title="@string/statistics_label"/>

View File

@ -52,7 +52,7 @@
<p>Created by Daniel Oeh</p>
<p>Copyright &copy; 2015 AntennaPod Contributors <a href="CONTRIBUTORS.txt">(View)</a></p>
<p>Copyright &copy; 2012-@year@ AntennaPod Contributors <a href="CONTRIBUTORS.txt">(View)</a></p>
<p>Licensed under the MIT License <a href="LICENSE.txt">(View)</a></p>
</div>

View File

@ -6,10 +6,10 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath "me.tatarka:gradle-retrolambda:3.2.4"
classpath "com.android.tools.build:gradle:2.2.3"
classpath "me.tatarka:gradle-retrolambda:3.3.1"
classpath "me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2"
classpath 'com.github.triplet.gradle:play-publisher:1.1.4'
classpath "com.github.triplet.gradle:play-publisher:1.1.4"
// Exclude the version that the android plugin depends on.
configurations.classpath.exclude group: "com.android.tools.external.lombok"
}
@ -51,9 +51,9 @@ project.ext {
glideOkhttpIntegrationVersion = "1.4.0"
iconifyVersion = "2.2.2"
jsoupVersion = "1.9.2"
materialDialogsVersion = "0.8.5.6@aar"
okhttpVersion = "2.7.5"
okioVersion = "1.9.0"
materialDialogsVersion = "0.8.5.8@aar"
okhttpVersion = "3.4.2"
okioVersion = "1.11.0"
recyclerviewFlexibledividerVersion = "1.2.6"
robotiumSoloVersion = "5.6.0"
rxAndroidVersion = "1.2.1"
@ -68,7 +68,7 @@ project.ext {
}
task wrapper(type: Wrapper) {
gradleVersion = "2.14.1"
gradleVersion = "3.2.1"
}
// free build hack: common functions

View File

@ -2,6 +2,8 @@ general:
artifacts:
- app/build/outputs/apk
machine:
environment:
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"'
java:
version: oraclejdk8
dependencies:
@ -16,4 +18,5 @@ dependencies:
test:
override:
- ./gradlew assembleDebug -PdisablePreDex
- ./gradlew assembleDebug -PdisablePreDex:
timeout: 1800

View File

@ -56,9 +56,9 @@ dependencies {
compile "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
compile "org.jsoup:jsoup:$jsoupVersion"
compile "com.github.bumptech.glide:glide:$glideVersion"
compile "com.github.bumptech.glide:okhttp-integration:$glideOkhttpIntegrationVersion"
compile "com.squareup.okhttp:okhttp:$okhttpVersion"
compile "com.squareup.okhttp:okhttp-urlconnection:$okhttpVersion"
compile "com.github.bumptech.glide:okhttp3-integration:$glideOkhttpIntegrationVersion@aar"
compile "com.squareup.okhttp3:okhttp:$okhttpVersion"
compile "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
compile "com.squareup.okio:okio:$okioVersion"
compile "com.nineoldandroids:library:2.4.0"
compile "de.greenrobot:eventbus:$eventbusVersion"

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.core;
import android.content.Context;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.NetworkUtils;
@ -43,7 +44,7 @@ public class ClientConfig {
UpdateManager.init(context);
PlaybackPreferences.init(context);
NetworkUtils.init(context);
// CastManager.init(context);
SleepTimerPreferences.init(context);
initialized = true;
}

View File

@ -38,10 +38,11 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> {
@Override
protected void onPostExecute(Void result) {
dialog.dismiss();
if(dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
if(skipOnCompletion) {
context.sendBroadcast(new Intent(
PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
context.sendBroadcast(new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE));
}
}

View File

@ -8,6 +8,7 @@ import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayOutputStream;
@ -27,10 +28,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlReader;
import de.danoeh.antennapod.core.export.opml.OpmlWriter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.opml.OpmlElement;
import de.danoeh.antennapod.core.opml.OpmlReader;
import de.danoeh.antennapod.core.opml.OpmlWriter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
@ -56,7 +57,9 @@ public class OpmlBackupAgent extends BackupAgentHelper {
}
}
/** Class for backing up and restoring the OPML file. */
/**
* Class for backing up and restoring the OPML file.
*/
private static class OpmlBackupHelper implements BackupHelper {
private static final String TAG = "OpmlBackupHelper";
@ -64,7 +67,9 @@ public class OpmlBackupAgent extends BackupAgentHelper {
private final Context mContext;
/** Checksum of restored OPML file */
/**
* Checksum of restored OPML file
*/
private byte[] mChecksum;
public OpmlBackupHelper(Context context) {
@ -170,12 +175,7 @@ public class OpmlBackupAgent extends BackupAgentHelper {
} catch (IOException e) {
Log.e(TAG, "Failed to restore OPML backup", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
IOUtils.closeQuietly(reader);
}
}

View File

@ -0,0 +1,21 @@
package de.danoeh.antennapod.core.event;
import android.support.annotation.Nullable;
public class MessageEvent {
public final String message;
@Nullable
public final Runnable action;
public MessageEvent(String message) {
this(message, null);
}
public MessageEvent(String message, Runnable action) {
this.message = message;
this.action = action;
}
}

View File

@ -0,0 +1,24 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.danoeh.antennapod.core.export;
public class CommonSymbols {
public static final String HEAD = "head";
public static final String BODY = "body";
public static final String TITLE = "title";
public static final String XML_FEATURE_INDENT_OUTPUT = "http://xmlpull.org/v1/doc/features.html#indent-output";
}

View File

@ -0,0 +1,29 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.danoeh.antennapod.core.export;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import de.danoeh.antennapod.core.feed.Feed;
public interface ExportWriter {
void writeDocument(List<Feed> feeds, Writer writer)
throws IllegalArgumentException, IllegalStateException, IOException;
String fileExtension();
}

View File

@ -0,0 +1,30 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.danoeh.antennapod.core.export.html;
import de.danoeh.antennapod.core.export.CommonSymbols;
class HtmlSymbols extends CommonSymbols {
static final String HTML = "html";
static final String ORDERED_LIST = "ol";
static final String LIST_ITEM = "li";
static String HEADING = "h1";
static final String LINK = "a";
static final String LINK_DESTINATION = "href";
}

View File

@ -0,0 +1,82 @@
package de.danoeh.antennapod.core.export.html;
import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.feed.Feed;
/** Writes HTML documents. */
public class HtmlWriter implements ExportWriter {
private static final String TAG = "HtmlWriter";
private static final String ENCODING = "UTF-8";
private static final String HTML_TITLE = "AntennaPod Subscriptions";
/**
* Takes a list of feeds and a writer and writes those into an HTML
* document.
*
* @throws IOException
* @throws IllegalStateException
* @throws IllegalArgumentException
*/
@Override
public void writeDocument(List<Feed> feeds, Writer writer)
throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document");
XmlSerializer xs = Xml.newSerializer();
xs.setFeature(HtmlSymbols.XML_FEATURE_INDENT_OUTPUT, true);
xs.setOutput(writer);
xs.startDocument(ENCODING, false);
xs.startTag(null, HtmlSymbols.HTML);
xs.startTag(null, HtmlSymbols.HEAD);
xs.startTag(null, HtmlSymbols.TITLE);
xs.text(HTML_TITLE);
xs.endTag(null, HtmlSymbols.TITLE);
xs.endTag(null, HtmlSymbols.HEAD);
xs.startTag(null, HtmlSymbols.BODY);
xs.startTag(null, HtmlSymbols.HEADING);
xs.text(HTML_TITLE);
xs.endTag(null, HtmlSymbols.HEADING);
xs.startTag(null, HtmlSymbols.ORDERED_LIST);
for (Feed feed : feeds) {
xs.startTag(null, HtmlSymbols.LIST_ITEM);
xs.text(feed.getTitle());
if (!TextUtils.isEmpty(feed.getLink())) {
xs.text(" [");
xs.startTag(null, HtmlSymbols.LINK);
xs.attribute(null, HtmlSymbols.LINK_DESTINATION, feed.getLink());
xs.text("Website");
xs.endTag(null, HtmlSymbols.LINK);
xs.text("]");
}
xs.text(" [");
xs.startTag(null, HtmlSymbols.LINK);
xs.attribute(null, HtmlSymbols.LINK_DESTINATION, feed.getDownload_url());
xs.text("Feed");
xs.endTag(null, HtmlSymbols.LINK);
xs.text("]");
xs.endTag(null, HtmlSymbols.LIST_ITEM);
}
xs.endTag(null, HtmlSymbols.ORDERED_LIST);
xs.endTag(null, HtmlSymbols.BODY);
xs.endTag(null, HtmlSymbols.HTML);
xs.endDocument();
Log.d(TAG, "Finished writing document");
}
public String fileExtension() {
return "html";
}
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.opml;
package de.danoeh.antennapod.core.export.opml;
/** Represents a single feed in an OPML file. */
public class OpmlElement {

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.opml;
package de.danoeh.antennapod.core.export.opml;
import android.util.Log;

View File

@ -0,0 +1,21 @@
package de.danoeh.antennapod.core.export.opml;
import de.danoeh.antennapod.core.export.CommonSymbols;
/** Contains symbols for reading and writing OPML documents. */
public final class OpmlSymbols extends CommonSymbols {
public static final String OPML = "opml";
static final String OUTLINE = "outline";
static final String TEXT = "text";
static final String XMLURL = "xmlUrl";
static final String HTMLURL = "htmlUrl";
static final String TYPE = "type";
static final String VERSION = "version";
static final String DATE_CREATED = "dateCreated";
private OpmlSymbols() {
}
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.opml;
package de.danoeh.antennapod.core.export.opml;
import android.util.Log;
import android.util.Xml;
@ -10,11 +10,13 @@ import java.io.Writer;
import java.util.Date;
import java.util.List;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.util.DateUtils;
/** Writes OPML documents. */
public class OpmlWriter {
public class OpmlWriter implements ExportWriter {
private static final String TAG = "OpmlWriter";
private static final String ENCODING = "UTF-8";
private static final String OPML_VERSION = "2.0";
@ -28,40 +30,29 @@ public class OpmlWriter {
* @throws IllegalStateException
* @throws IllegalArgumentException
*/
@Override
public void writeDocument(List<Feed> feeds, Writer writer)
throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document");
XmlSerializer xs = Xml.newSerializer();
xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true);
xs.setOutput(writer);
xs.startDocument(ENCODING, false);
xs.text("\n");
xs.startTag(null, OpmlSymbols.OPML);
xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION);
xs.text("\n");
xs.text(" ");
xs.startTag(null, OpmlSymbols.HEAD);
xs.text("\n");
xs.text(" ");
xs.startTag(null, OpmlSymbols.TITLE);
xs.text(OPML_TITLE);
xs.endTag(null, OpmlSymbols.TITLE);
xs.text("\n");
xs.text(" ");
xs.startTag(null, OpmlSymbols.DATE_CREATED);
xs.text(DateUtils.formatRFC822Date(new Date()));
xs.endTag(null, OpmlSymbols.DATE_CREATED);
xs.text("\n");
xs.text(" ");
xs.endTag(null, OpmlSymbols.HEAD);
xs.text("\n");
xs.text(" ");
xs.startTag(null, OpmlSymbols.BODY);
xs.text("\n");
for (Feed feed : feeds) {
xs.text(" ");
xs.startTag(null, OpmlSymbols.OUTLINE);
xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle());
xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle());
@ -73,14 +64,15 @@ public class OpmlWriter {
xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink());
}
xs.endTag(null, OpmlSymbols.OUTLINE);
xs.text("\n");
}
xs.text(" ");
xs.endTag(null, OpmlSymbols.BODY);
xs.text("\n");
xs.endTag(null, OpmlSymbols.OPML);
xs.text("\n");
xs.endDocument();
Log.d(TAG, "Finished writing document");
}
public String fileExtension() {
return "opml";
}
}

View File

@ -26,7 +26,11 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public static final String TYPE_RSS091 = "rss";
public static final String TYPE_ATOM1 = "atom";
private String title;
/* title as defined by the feed */
private String feedTitle;
/* custom title set by the user */
private String customTitle;
/**
* Contains 'id'-element in Atom feed.
*/
@ -92,13 +96,14 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
/**
* This constructor is used for restoring a feed from the database.
*/
public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink,
public Feed(long id, String lastUpdate, String title, String customTitle, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink,
String filter, boolean lastUpdateFailed) {
super(fileUrl, downloadUrl, downloaded);
this.id = id;
this.title = title;
this.feedTitle = title;
this.customTitle = customTitle;
this.lastUpdate = lastUpdate;
this.link = link;
this.description = description;
@ -126,7 +131,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl,
String downloadUrl, boolean downloaded) {
this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image,
this(id, lastUpdate, title, null, link, description, paymentLink, author, language, type, feedIdentifier, image,
fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null, null, false);
}
@ -154,7 +159,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
*/
public Feed(String url, String lastUpdate, String title) {
this(url, lastUpdate);
this.title = title;
this.feedTitle = title;
this.flattrStatus = new FlattrStatus();
}
@ -171,6 +176,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
@ -191,6 +197,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
cursor.getLong(indexId),
cursor.getString(indexLastUpdate),
cursor.getString(indexTitle),
cursor.getString(indexCustomTitle),
cursor.getString(indexLink),
cursor.getString(indexDescription),
cursor.getString(indexPaymentLink),
@ -268,8 +275,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
return feedIdentifier;
} else if (download_url != null && !download_url.isEmpty()) {
return download_url;
} else if (title != null && !title.isEmpty()) {
return title;
} else if (feedTitle != null && !feedTitle.isEmpty()) {
return feedTitle;
} else {
return link;
}
@ -277,8 +284,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
@Override
public String getHumanReadableIdentifier() {
if (title != null) {
return title;
if (feedTitle != null) {
return feedTitle;
} else {
return download_url;
}
@ -287,8 +294,8 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
public void updateFromOther(Feed other) {
// don't update feed's download_url, we do that manually if redirected
// see AntennapodHttpClient
if (other.title != null) {
title = other.title;
if (other.feedTitle != null) {
feedTitle = other.feedTitle;
}
if (other.feedIdentifier != null) {
feedIdentifier = other.feedIdentifier;
@ -323,7 +330,7 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
if (super.compareWithOther(other)) {
return true;
}
if (!TextUtils.equals(title, other.title)) {
if (!TextUtils.equals(feedTitle, other.feedTitle)) {
return true;
}
if (other.feedIdentifier != null) {
@ -384,11 +391,28 @@ public class Feed extends FeedFile implements FlattrThing, ImageResource {
}
public String getTitle() {
return title;
return !TextUtils.isEmpty(customTitle) ? customTitle : feedTitle;
}
public void setTitle(String title) {
this.title = title;
this.feedTitle = title;
}
public String getFeedTitle() {
return this.feedTitle;
}
@Nullable
public String getCustomTitle() {
return this.customTitle;
}
public void setCustomTitle(String customTitle) {
if(customTitle == null || customTitle.equals(feedTitle)) {
this.customTitle = null;
} else {
this.customTitle = customTitle;
}
}
public String getLink() {

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.core.feed;
import android.database.Cursor;
import android.support.annotation.Nullable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@ -269,6 +270,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
}
}
@Nullable
public FeedMedia getMedia() {
return media;
}

View File

@ -18,6 +18,7 @@ public class FeedItemFilter {
private boolean showNotQueued = false;
private boolean showDownloaded = false;
private boolean showNotDownloaded = false;
private boolean showHasMedia = false;
public FeedItemFilter(String properties) {
this(TextUtils.split(properties, ","));
@ -49,6 +50,9 @@ public class FeedItemFilter {
case "not_downloaded":
showNotDownloaded = true;
break;
case "has_media":
showHasMedia = true;
break;
}
}
}
@ -82,6 +86,8 @@ public class FeedItemFilter {
if (showDownloaded && !downloaded) continue;
if (showNotDownloaded && downloaded) continue;
if (showHasMedia && !item.hasMedia()) continue;
// If the item reaches here, it meets all criteria
result.add(item);
}

View File

@ -4,15 +4,12 @@ import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.bumptech.glide.integration.okhttp.OkHttpStreamFetcher;
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GenericLoaderFactory;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.io.InputStream;
@ -22,9 +19,13 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.NetworkUtils;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* @see com.bumptech.glide.integration.okhttp.OkHttpUrlLoader
* @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
*/
public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@ -42,9 +43,10 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
if (internalClient == null) {
synchronized (Factory.class) {
if (internalClient == null) {
internalClient = AntennapodHttpClient.newHttpClient();
internalClient.interceptors().add(new NetworkAllowanceInterceptor());
internalClient.interceptors().add(new BasicAuthenticationInterceptor());
OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder();
builder.interceptors().add(new NetworkAllowanceInterceptor());
builder.interceptors().add(new BasicAuthenticationInterceptor());
internalClient = builder.build();
}
}
}
@ -113,8 +115,8 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@Override
public Response intercept(Chain chain) throws IOException {
com.squareup.okhttp.Request request = chain.request();
String url = request.urlString();
Request request = chain.request();
String url = request.url().toString();
String authentication = DBReader.getImageAuthentication(url);
if(TextUtils.isEmpty(authentication)) {
@ -125,7 +127,7 @@ public class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
// add authentication
String[] auth = authentication.split(":");
String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1");
com.squareup.okhttp.Request newRequest = request
Request newRequest = request
.newBuilder()
.addHeader("Authorization", credentials)
.build();

View File

@ -1,15 +1,6 @@
package de.danoeh.antennapod.core.gpoddernet;
import android.support.annotation.NonNull;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -38,6 +29,13 @@ import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
@ -570,15 +568,7 @@ public class GpodnetService {
e.printStackTrace();
throw new GpodnetServiceException(e);
} finally {
if (response != null && body != null) {
try {
body.close();
} catch (IOException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
}
body.close();
}
return responseString;
}
@ -605,12 +595,7 @@ public class GpodnetService {
throw new GpodnetServiceException(e);
} finally {
if (body != null) {
try {
body.close();
} catch (IOException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
}
body.close();
}
}
return result;
@ -619,12 +604,7 @@ public class GpodnetService {
private String getStringFromResponseBody(@NonNull ResponseBody body)
throws GpodnetServiceException {
ByteArrayOutputStream outputStream;
int contentLength = 0;
try {
contentLength = (int) body.contentLength();
} catch (IOException ignore) {
// ignore
}
int contentLength = (int) body.contentLength();
if (contentLength > 0) {
outputStream = new ByteArrayOutputStream(contentLength);
} else {

View File

@ -1,22 +0,0 @@
package de.danoeh.antennapod.core.opml;
/** Contains symbols for reading and writing OPML documents. */
public final class OpmlSymbols {
public static final String OPML = "opml";
public static final String BODY = "body";
public static final String OUTLINE = "outline";
public static final String TEXT = "text";
public static final String XMLURL = "xmlUrl";
public static final String HTMLURL = "htmlUrl";
public static final String TYPE = "type";
public static final String VERSION = "version";
public static final String HEAD = "head";
public static final String TITLE = "title";
public static final String DATE_CREATED = "dateCreated";
private OpmlSymbols() {
}
}

View File

@ -0,0 +1,80 @@
package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.concurrent.TimeUnit;
public class SleepTimerPreferences {
private static final String TAG = "SleepTimerPreferences";
private static final String PREF_NAME = "SleepTimerDialog";
private static final String PREF_VALUE = "LastValue";
private static final String PREF_TIME_UNIT = "LastTimeUnit";
private static final String PREF_VIBRATE = "Vibrate";
private static final String PREF_SHAKE_TO_RESET = "ShakeToReset";
private static final String PREF_AUTO_ENABLE = "AutoEnable";
private static final TimeUnit[] UNITS = { TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS };
private static final String DEFAULT_VALUE = "15";
private static final int DEFAULT_TIME_UNIT = 1;
private static Context context;
private static SharedPreferences prefs;
/**
* Sets up the UserPreferences class.
*
* @throws IllegalArgumentException if context is null
*/
public static void init(@NonNull Context context) {
Log.d(TAG, "Creating new instance of SleepTimerPreferences");
SleepTimerPreferences.prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public static void setLastTimer(String value, int timeUnit) {
prefs.edit().putString(PREF_VALUE, value).putInt(PREF_TIME_UNIT, timeUnit).apply();
}
public static String lastTimerValue() {
return prefs.getString(PREF_VALUE, DEFAULT_VALUE);
}
public static int lastTimerTimeUnit() {
return prefs.getInt(PREF_TIME_UNIT, DEFAULT_TIME_UNIT);
}
public static long timerMillis() {
long value = Long.parseLong(lastTimerValue());
return UNITS[lastTimerTimeUnit()].toMillis(value);
}
public static void setVibrate(boolean vibrate) {
prefs.edit().putBoolean(PREF_VIBRATE, vibrate).apply();
}
public static boolean vibrate() {
return prefs.getBoolean(PREF_VIBRATE, true);
}
public static void setShakeToReset(boolean shakeToReset) {
prefs.edit().putBoolean(PREF_SHAKE_TO_RESET, shakeToReset).apply();
}
public static boolean shakeToReset() {
return prefs.getBoolean(PREF_SHAKE_TO_RESET, true);
}
public static void setAutoEnable(boolean autoEnable) {
prefs.edit().putBoolean(PREF_AUTO_ENABLE, autoEnable).apply();
}
public static boolean autoEnable() {
return prefs.getBoolean(PREF_AUTO_ENABLE, false);
}
}

View File

@ -56,7 +56,6 @@ public class UserPreferences {
public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground";
public static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport";
// Queue
public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront";
@ -65,6 +64,7 @@ public class UserPreferences {
public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect";
public static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect";
public static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips";
public static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts";
public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode";
public static final String PREF_AUTO_DELETE = "prefAutoDelete";
@ -74,6 +74,7 @@ public class UserPreferences {
public static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall";
// Network
public static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded";
public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup";
@ -122,13 +123,14 @@ public class UserPreferences {
private static final int NOTIFICATION_BUTTON_FAST_FORWARD = 1;
private static final int NOTIFICATION_BUTTON_SKIP = 2;
private static int EPISODE_CACHE_SIZE_UNLIMITED = -1;
public static int FEED_ORDER_COUNTER = 0;
public static int FEED_ORDER_ALPHABETICAL = 1;
public static int FEED_ORDER_LAST_UPDATE = 2;
public static int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
public static int FEED_COUNTER_SHOW_NEW = 1;
public static int FEED_COUNTER_SHOW_UNPLAYED = 2;
public static int FEED_COUNTER_SHOW_NONE = 3;
public static final int FEED_ORDER_COUNTER = 0;
public static final int FEED_ORDER_ALPHABETICAL = 1;
public static final int FEED_ORDER_LAST_UPDATE = 2;
public static final int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0;
public static final int FEED_COUNTER_SHOW_NEW = 1;
public static final int FEED_COUNTER_SHOW_UNPLAYED = 2;
public static final int FEED_COUNTER_SHOW_NONE = 3;
public static final int FEED_COUNTER_SHOW_DOWNLOADED = 4;
private static Context context;
private static SharedPreferences prefs;
@ -257,11 +259,10 @@ public class UserPreferences {
return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true);
}
/**
* Returns {@code true} if new queue elements are added to the front
*
* @return {@code true} if new queue elements are added to the front; {@code false} otherwise
*/
public static boolean enqueueDownloadedEpisodes() {
return prefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true);
}
public static boolean enqueueAtFront() {
return prefs.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
}
@ -282,6 +283,10 @@ public class UserPreferences {
return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false);
}
public static boolean shouldHardwarePreviousButtonRestart() {
return prefs.getBoolean(PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS, false);
}
public static boolean isFollowQueue() {
return prefs.getBoolean(PREF_FOLLOW_QUEUE, true);
@ -332,7 +337,10 @@ public class UserPreferences {
}
/*
* Returns update interval in milliseconds; value 0 means that auto update is disabled
* or feeds are updated at a certain time of day
*/
public static long getUpdateInterval() {
String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0");
if(!updateInterval.contains(":")) {
@ -754,12 +762,12 @@ public class UserPreferences {
if (timeOfDay.length == 2) {
restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
} else {
long hours = getUpdateInterval();
long startTrigger = hours;
long milliseconds = getUpdateInterval();
long startTrigger = milliseconds;
if (now) {
startTrigger = TimeUnit.SECONDS.toMillis(10);
}
restartUpdateIntervalAlarm(startTrigger, hours);
restartUpdateIntervalAlarm(startTrigger, milliseconds);
}
}

View File

@ -5,12 +5,6 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.http.StatusLine;
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
@ -20,16 +14,27 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.http.StatusLine;
/**
* Provides access to a HttpClient singleton.
@ -50,13 +55,13 @@ public class AntennapodHttpClient {
*/
public static synchronized OkHttpClient getHttpClient() {
if (httpClient == null) {
httpClient = newHttpClient();
httpClient = newBuilder().build();
}
return httpClient;
}
public static synchronized void reinit() {
httpClient = newHttpClient();
httpClient = newBuilder().build();
}
/**
@ -67,33 +72,33 @@ public class AntennapodHttpClient {
* @return http client
*/
@NonNull
public static OkHttpClient newHttpClient() {
public static OkHttpClient.Builder newBuilder() {
Log.d(TAG, "Creating new instance of HTTP client");
System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
OkHttpClient client = new OkHttpClient();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// detect 301 Moved permanently and 308 Permanent Redirect
client.networkInterceptors().add(chain -> {
builder.networkInterceptors().add(chain -> {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_MOVED_PERM ||
response.code() == StatusLine.HTTP_PERM_REDIRECT) {
String location = response.header("Location");
if (location.startsWith("/")) { // URL is not absolute, but relative
URL url = request.url();
location = url.getProtocol() + "://" + url.getHost() + location;
HttpUrl url = request.url();
location = url.scheme() + "://" + url.host() + location;
} else if (!location.toLowerCase().startsWith("http://") &&
!location.toLowerCase().startsWith("https://")) {
// Reference is relative to current path
URL url = request.url();
String path = url.getPath();
HttpUrl url = request.url();
String path = url.encodedPath();
String newPath = path.substring(0, path.lastIndexOf("/") + 1) + location;
location = url.getProtocol() + "://" + url.getHost() + newPath;
location = url.scheme() + "://" + url.host() + newPath;
}
try {
DBWriter.updateFeedDownloadURL(request.urlString(), location).get();
DBWriter.updateFeedDownloadURL(request.url().toString(), location).get();
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
@ -104,26 +109,26 @@ public class AntennapodHttpClient {
// set cookie handler
CookieManager cm = new CookieManager();
cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
client.setCookieHandler(cm);
builder.cookieJar(new JavaNetCookieJar(cm));
// set timeouts
client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
builder.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
builder.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
builder.writeTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
// configure redirects
client.setFollowRedirects(true);
client.setFollowSslRedirects(true);
builder.followRedirects(true);
builder.followSslRedirects(true);
ProxyConfig config = UserPreferences.getProxyConfig();
if (config.type != Proxy.Type.DIRECT) {
int port = config.port > 0 ? config.port : ProxyConfig.DEFAULT_PORT;
SocketAddress address = InetSocketAddress.createUnresolved(config.host, port);
Proxy proxy = new Proxy(config.type, address);
client.setProxy(proxy);
builder.proxy(proxy);
if (!TextUtils.isEmpty(config.username)) {
String credentials = Credentials.basic(config.username, config.password);
client.interceptors().add(chain -> {
builder.interceptors().add(chain -> {
Request request = chain.request().newBuilder()
.header("Proxy-Authorization", credentials).build();
return chain.proceed(request);
@ -131,9 +136,9 @@ public class AntennapodHttpClient {
}
}
if(16 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 21) {
client.setSslSocketFactory(new CustomSslSocketFactory());
builder.sslSocketFactory(new CustomSslSocketFactory(), trustManager());
}
return client;
return builder;
}
/**
@ -146,6 +151,23 @@ public class AntennapodHttpClient {
}
}
private static X509TrustManager trustManager() {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
return null;
}
}
private static class CustomSslSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;

View File

@ -842,11 +842,14 @@ public class DownloadService extends Service {
successful = false;
reason = DownloadError.ERROR_PARSER_EXCEPTION;
reasonDetailed = e.getMessage();
} finally {
File feedFile = new File(request.getDestination());
if(feedFile.exists()) {
boolean deleted = feedFile.delete();
Log.d(TAG, "Deletion of file '" + feedFile.getAbsolutePath() + "' " + (deleted ? "successful" : "FAILED"));
}
}
// cleanup();
if (successful) {
// we create a 'successful' download log if the feed's last refresh failed
List<DownloadStatus> log = DBReader.getFeedDownloadLog(feed);
@ -1048,7 +1051,8 @@ public class DownloadService extends Service {
DBWriter.setFeedMedia(media).get();
if (item != null && !DBTasks.isInQueue(DownloadService.this, item.getId())) {
if (item != null && UserPreferences.enqueueDownloadedEpisodes() &&
!DBTasks.isInQueue(DownloadService.this, item.getId())) {
DBWriter.addQueueItem(DownloadService.this, item).get();
}
} catch (ExecutionException | InterruptedException e) {

View File

@ -2,21 +2,7 @@ package de.danoeh.antennapod.core.service.download;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URIUtil;
import okio.ByteString;
import org.apache.commons.io.IOUtils;
import java.io.BufferedInputStream;
@ -32,6 +18,22 @@ import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.URIUtil;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.ByteString;
public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
@ -57,8 +59,9 @@ public class HttpDownloader extends Downloader {
}
}
OkHttpClient httpClient = AntennapodHttpClient.newHttpClient();
httpClient.interceptors().add(new BasicAuthorizationInterceptor(request));
OkHttpClient.Builder httpClientBuilder = AntennapodHttpClient.newBuilder();
httpClientBuilder.interceptors().add(new BasicAuthorizationInterceptor(request));
OkHttpClient httpClient = httpClientBuilder.build();
RandomAccessFile out = null;
InputStream connection;
ResponseBody responseBody = null;
@ -103,7 +106,9 @@ public class HttpDownloader extends Downloader {
} catch (IOException e) {
Log.e(TAG, e.toString());
if (e.getMessage().contains("PROTOCOL_ERROR")) {
httpClient.setProtocols(Collections.singletonList(Protocol.HTTP_1_1));
httpClient = httpClient.newBuilder()
.protocols(Collections.singletonList(Protocol.HTTP_1_1))
.build();
response = httpClient.newCall(httpReq.build()).execute();
} else {
throw e;

View File

@ -146,7 +146,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
if (!media.getIdentifier().equals(playable.getIdentifier())) {
final Playable oldMedia = media;
executor.submit(() -> callback.onPostPlayback(oldMedia, false, true));
executor.submit(() -> callback.onPostPlayback(oldMedia, false, false, true));
}
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
@ -383,6 +383,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
statusBeforeSeeking = playerStatus;
setPlayerStatus(PlayerStatus.SEEKING, media, getPosition());
mediaPlayer.seekTo(t);
if (statusBeforeSeeking == PlayerStatus.PREPARED) {
media.setPosition(t);
}
try {
seekLatch.await(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
@ -755,7 +758,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
@Override
protected Future<?> endPlayback(final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) {
protected Future<?> endPlayback(final boolean hasEnded, final boolean wasSkipped,
final boolean shouldContinue, final boolean toStoppedState) {
return executor.submit(() -> {
playerLock.lock();
releaseWifiLockIfNecessary();
@ -813,7 +817,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
final boolean hasNext = nextMedia != null;
executor.submit(() -> callback.onPostPlayback(currentMedia, !wasSkipped, hasNext));
executor.submit(() -> callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext));
} else if (isPlaying) {
callback.onPlaybackPause(currentMedia, currentMedia.getPosition());
}
@ -848,27 +852,24 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
private IPlayer setMediaPlayerListeners(IPlayer mp) {
if (mp != null && media != null) {
if (media.getMediaType() == MediaType.AUDIO) {
((AudioPlayer) mp)
.setOnCompletionListener(audioCompletionListener);
((AudioPlayer) mp)
.setOnSeekCompleteListener(audioSeekCompleteListener);
((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
((AudioPlayer) mp)
.setOnBufferingUpdateListener(audioBufferingUpdateListener);
((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
((AudioPlayer) mp).setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
} else {
((VideoPlayer) mp)
.setOnCompletionListener(videoCompletionListener);
((VideoPlayer) mp)
.setOnSeekCompleteListener(videoSeekCompleteListener);
((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
((VideoPlayer) mp)
.setOnBufferingUpdateListener(videoBufferingUpdateListener);
((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
}
if (mp == null || media == null) {
return mp;
}
if (media.getMediaType() == MediaType.VIDEO) {
VideoPlayer vp = (VideoPlayer) mp;
vp.setOnCompletionListener(videoCompletionListener);
vp.setOnSeekCompleteListener(videoSeekCompleteListener);
vp.setOnErrorListener(videoErrorListener);
vp.setOnBufferingUpdateListener(videoBufferingUpdateListener);
vp.setOnInfoListener(videoInfoListener);
} else {
AudioPlayer ap = (AudioPlayer) mp;
ap.setOnCompletionListener(audioCompletionListener);
ap.setOnSeekCompleteListener(audioSeekCompleteListener);
ap.setOnErrorListener(audioErrorListener);
ap.setOnBufferingUpdateListener(audioBufferingUpdateListener);
ap.setOnInfoListener(audioInfoListener);
ap.setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
}
return mp;
}
@ -880,7 +881,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
mp -> genericOnCompletion();
private void genericOnCompletion() {
endPlayback(false, true, true);
endPlayback(true, false, true, true);
}
private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener =

View File

@ -47,12 +47,14 @@ import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.feed.Chapter;
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.glide.ApGlideSettings;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.storage.DBReader;
@ -62,6 +64,7 @@ import de.danoeh.antennapod.core.util.IntList;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.greenrobot.event.EventBus;
/**
* Controls the MediaPlayer that plays a FeedMedia-file
@ -298,7 +301,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
List<MediaSessionCompat.QueueItem> queueItems = new ArrayList<>();
try {
for (FeedItem feedItem : taskManager.getQueue()) {
queueItems.add(new MediaSessionCompat.QueueItem(feedItem.getMedia().getMediaItem().getDescription(), feedItem.getId()));
if(feedItem.getMedia() != null) {
MediaDescriptionCompat mediaDescription = feedItem.getMedia().getMediaItem().getDescription();
queueItems.add(new MediaSessionCompat.QueueItem(mediaDescription, feedItem.getId()));
}
}
mediaSession.setQueue(queueItems);
} catch (InterruptedException e) {
@ -478,6 +484,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000);
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
if(UserPreferences.shouldHardwarePreviousButtonRestart()) {
// user wants to restart current episode
mediaPlayer.seekTo(0);
} else {
// user wants to rewind current episode
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
}
break;
case KeyEvent.KEYCODE_MEDIA_REWIND:
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
break;
@ -597,6 +611,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
writePlayerStatusPlaybackPreferences();
setupNotification(newInfo);
started = true;
// set sleep timer if auto-enabled
if(newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING &&
SleepTimerPreferences.autoEnable() && !sleepTimerActive()) {
setSleepTimer(SleepTimerPreferences.timerMillis(), SleepTimerPreferences.shakeToReset(),
SleepTimerPreferences.vibrate());
}
break;
case ERROR:
@ -669,8 +689,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext) {
PlaybackService.this.onPostPlayback(media, ended, playingNext);
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
boolean playingNext) {
PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext);
}
@Override
@ -768,16 +789,20 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Even though these tasks aren't supposed to be resource intensive, a good practice is to
* usually call this method on a background thread.
*
* @param playable the media object that was playing. It is assumed that its position property
* was updated before this method was called.
* @param ended if true, it signals that {@param playable} was played until its end.
* In such case, the position property of the media becomes irrelevant for most of
* the tasks (although it's still a good practice to keep it accurate).
* @param playingNext if true, it means another media object is being loaded in place of this one.
* @param playable the media object that was playing. It is assumed that its position
* property was updated before this method was called.
* @param ended if true, it signals that {@param playable} was played until its end.
* In such case, the position property of the media becomes irrelevant for
* most of the tasks (although it's still a good practice to keep it
* accurate).
* @param skipped if the user pressed a skip >| button.
* @param playingNext if true, it means another media object is being loaded in place of this
* one.
* Instances when we'd set it to false would be when we're not following the
* queue or when the queue has ended.
*/
private void onPostPlayback(final Playable playable, boolean ended, boolean playingNext) {
private void onPostPlayback(final Playable playable, boolean ended, boolean skipped,
boolean playingNext) {
if (playable == null) {
Log.e(TAG, "Cannot do post-playback processing: media was null");
return;
@ -808,7 +833,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (item != null) {
if (ended || smartMarkAsPlayed ||
!UserPreferences.shouldSkipKeepEpisode()) {
(skipped && !UserPreferences.shouldSkipKeepEpisode())) {
// only mark the item as played if we're not keeping it anyways
DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended);
try {
@ -829,7 +854,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
}
if (ended || playingNext) {
if (ended || skipped || playingNext) {
DBWriter.addItemToPlaybackHistory(media);
}
}
@ -838,11 +863,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate);
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label),
() -> disableSleepTimer()));
}
public void disableSleepTimer() {
taskManager.disableSleepTimer();
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_disabled_label)));
}
private void writePlaybackPreferencesNoMediaPlaying() {
@ -996,13 +1024,37 @@ public class PlaybackService extends MediaBrowserServiceCompat {
state = PlaybackStateCompat.STATE_NONE;
}
sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed());
sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE
long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_FAST_FORWARD
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
if (useSkipToPreviousForRewindInLockscreen()) {
// Workaround to fool Android so that Lockscreen will expose a skip-to-previous button,
// which will be used for rewind.
// The workaround is used for pre Lollipop (Androidv5) devices.
// For Androidv5+, lockscreen widges are really notifications (compact),
// with an independent codepath
//
// @see #sessionCallback in the backing callback, skipToPrevious implementation
// is actually the same as rewind. So no new inconsistency is created.
// @see #setupNotification() for the method to create Androidv5+ lockscreen UI
// with notification (compact)
capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
sessionState.setActions(capabilities);
mediaSession.setPlaybackState(sessionState.build());
}
private static boolean useSkipToPreviousForRewindInLockscreen() {
// showRewindOnCompactNotification() corresponds to the "Set Lockscreen Buttons"
// Settings in UI.
// Hence, from user perspective, he/she is setting the buttons for Lockscreen
return ( UserPreferences.showRewindOnCompactNotification() &&
(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) );
}
/**
* Used by updateMediaSessionMetadata to load notification data in another thread.
*/
@ -1050,7 +1102,13 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT));
mediaSession.setMetadata(builder.build());
try {
mediaSession.setMetadata(builder.build());
} catch (OutOfMemoryError e) {
Log.e(TAG, "Setting media session metadata", e);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null);
mediaSession.setMetadata(builder.build());
}
}
};
@ -1285,7 +1343,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (info.playable != null) {
Intent i = new Intent(whatChanged);
i.putExtra("id", 1);
i.putExtra("id", 1L);
i.putExtra("artist", "");
i.putExtra("album", info.playable.getFeedTitle());
i.putExtra("track", info.playable.getEpisodeTitle());
@ -1294,8 +1352,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (queue != null) {
i.putExtra("ListSize", queue.size());
}
i.putExtra("duration", info.playable.getDuration());
i.putExtra("position", info.playable.getPosition());
i.putExtra("duration", (long) info.playable.getDuration());
i.putExtra("position", (long) info.playable.getPosition());
sendBroadcast(i);
}
}
@ -1375,7 +1433,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
*/
private void pauseIfPauseOnDisconnect() {
if (UserPreferences.isPauseOnHeadsetDisconnect()) {
if (UserPreferences.isPauseOnHeadsetDisconnect() && !isCasting()) {
if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
transientPause = true;
}

View File

@ -29,9 +29,10 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* Return value of some PSMP methods if the method call failed.
*/
public static final int INVALID_TIME = -1;
static final int INVALID_TIME = -1;
protected volatile PlayerStatus playerStatus;
volatile PlayerStatus oldPlayerStatus;
volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
@ -41,8 +42,8 @@ public abstract class PlaybackServiceMediaPlayer {
protected final PSMPCallback callback;
protected final Context context;
public PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){
PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
@ -204,7 +205,7 @@ public abstract class PlaybackServiceMediaPlayer {
* @return The PSMPInfo object.
*/
public final synchronized PSMPInfo getPSMPInfo() {
return new PSMPInfo(playerStatus, getPlayable());
return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable());
}
/**
@ -228,32 +229,35 @@ public abstract class PlaybackServiceMediaPlayer {
protected abstract void setPlayable(Playable playable);
public void skip() {
endPlayback(true, true, true);
endPlayback(false, true, true, true);
}
/**
* Ends playback of current media (if any) and moves into INDETERMINATE state, unless
* {@param toStoppedState} is set to true, in which case it moves into STOPPED state.
*
* @see #endPlayback(boolean, boolean, boolean)
* @see #endPlayback(boolean, boolean, boolean, boolean)
*/
public Future<?> stopPlayback(boolean toStoppedState) {
return endPlayback(true, false, toStoppedState);
return endPlayback(false, false, false, toStoppedState);
}
/**
* Internal method that handles end of playback.
*
* Currently, it has 4 use cases:
* Currently, it has 5 use cases:
* <ul>
* <li>Media playback has completed: call with (false, true, true)</li>
* <li>User asks to skip to next episode: call with (true, true, true)</li>
* <li>Stopping the media player: call with (true, false, true)</li>
* <li>We want to change the media player implementation: call with (true, false, false)</li>
* <li>Media playback has completed: call with (true, false, true, true)</li>
* <li>User asks to skip to next episode: call with (false, true, true, true)</li>
* <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li>
* <li>Stopping the media player: call with (false, false, false, true)</li>
* <li>We want to change the media player implementation: call with (false, false, false, false)</li>
* </ul>
*
* @param wasSkipped If true, we assume the current media's playback has ended, for
* @param hasEnded If true, we assume the current media's playback has ended, for
* purposes of post playback processing.
* @param wasSkipped Whether the user chose to skip the episode (by pressing the skip
* button).
* @param shouldContinue If true, the media player should try to load, and possibly play,
* the next item, based on the user preferences and whether such item
* exists.
@ -264,14 +268,15 @@ public abstract class PlaybackServiceMediaPlayer {
*
* @return a Future, just for the purpose of tracking its execution.
*/
protected abstract Future<?> endPlayback(boolean wasSkipped, boolean shouldContinue, boolean toStoppedState);
protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped,
boolean shouldContinue, boolean toStoppedState);
/**
* @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
*/
protected abstract boolean shouldLockWifi();
protected final synchronized void acquireWifiLockIfNecessary() {
final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
@ -282,7 +287,7 @@ public abstract class PlaybackServiceMediaPlayer {
}
}
protected final synchronized void releaseWifiLockIfNecessary() {
final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
@ -303,29 +308,28 @@ public abstract class PlaybackServiceMediaPlayer {
* @param position The position to be set to the current Playable object in case playback started or paused.
* Will be ignored if given the value of {@link #INVALID_TIME}.
*/
protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
PlayerStatus oldStatus = playerStatus;
this.oldPlayerStatus = playerStatus;
this.playerStatus = newStatus;
setPlayable(newMedia);
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
if (oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
callback.onPlaybackPause(newMedia, position);
} else if (oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
} else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
callback.onPlaybackStart(newMedia, position);
}
}
callback.statusChanged(new PSMPInfo(playerStatus, getPlayable()));
callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()));
}
/**
* @see #setPlayerStatus(PlayerStatus, Playable, int)
*/
protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
setPlayerStatus(newStatus, newMedia, INVALID_TIME);
}
@ -346,7 +350,7 @@ public abstract class PlaybackServiceMediaPlayer {
boolean onMediaPlayerError(Object inObj, int what, int extra);
void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext);
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
void onPlaybackStart(@NonNull Playable playable, int position);
@ -361,10 +365,12 @@ public abstract class PlaybackServiceMediaPlayer {
* Holds information about a PSMP object.
*/
public static class PSMPInfo {
public PlayerStatus oldPlayerStatus;
public PlayerStatus playerStatus;
public Playable playable;
public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
this.oldPlayerStatus = oldPlayerStatus;
this.playerStatus = playerStatus;
this.playable = playable;
}

View File

@ -1063,7 +1063,7 @@ public final class DBReader {
// reverse natural order: podcast with most unplayed episodes first
return -1;
} else if(counterLhs == counterRhs) {
return lhs.getTitle().compareTo(rhs.getTitle());
return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
} else {
return 1;
}
@ -1077,7 +1077,7 @@ public final class DBReader {
} else if(t2 == null) {
return -1;
} else {
return t1.toLowerCase().compareTo(t2.toLowerCase());
return t1.compareToIgnoreCase(t2);
}
};
} else {

View File

@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.util.Log;
@ -16,7 +17,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.core.ClientConfig;
@ -27,21 +28,29 @@ 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.feed.FeedPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
import static android.content.Context.MODE_PRIVATE;
import static android.provider.Contacts.SettingsColumns.KEY;
/**
* Provides methods for doing common tasks that use DBReader and DBWriter.
*/
public final class DBTasks {
private static final String TAG = "DBTasks";
public static final String PREF_NAME = "dbtasks";
private static final String PREF_LAST_REFRESH = "last_refresh";
/**
* Executor service used by the autodownloadUndownloadedEpisodes method.
*/
@ -162,6 +171,9 @@ public final class DBTasks {
}
isRefreshing.set(false);
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
if (FlattrUtils.hasToken()) {
Log.d(TAG, "Flattring all pending things.");
new FlattrClickWorker(context).executeAsync(); // flattr pending things
@ -313,6 +325,31 @@ public final class DBTasks {
DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force);
}
/*
* Checks if the app should refresh all feeds, i.e. if the last auto refresh failed.
*
* The feeds are only refreshed if an update interval or time of day is set and the last
* (successful) refresh was before the last interval or more than a day ago, respectively.
*/
public static void checkShouldRefreshFeeds(Context context) {
long interval = 0;
if(UserPreferences.getUpdateInterval() > 0) {
interval = UserPreferences.getUpdateInterval();
} else if(UserPreferences.getUpdateTimeOfDay().length > 0){
interval = TimeUnit.DAYS.toMillis(1);
}
if(interval == 0) { // auto refresh is disabled
return;
}
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
long lastRefresh = prefs.getLong(PREF_LAST_REFRESH, 0);
Log.d(TAG, "last refresh: " + Converter.getDurationStringLocalized(context,
System.currentTimeMillis() - lastRefresh) + " ago");
if(lastRefresh <= System.currentTimeMillis() - interval) {
DBTasks.refreshAllFeeds(context, null);
}
}
/**
* Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the
* DB and send a FeedUpdateBroadcast.

View File

@ -884,6 +884,17 @@ public class DBWriter {
});
}
public static Future<?> setFeedCustomTitle(Feed feed) {
return dbExec.submit(() -> {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
adapter.setFeedCustomTitle(feed.getId(), feed.getCustomTitle());
adapter.close();
EventDistributor.getInstance().sendFeedUpdateBroadcast();
});
}
/**
* format an url for querying the database
* (postfix a / and apply percent-encoding)

View File

@ -4,7 +4,9 @@ import android.content.Context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@ -51,15 +53,17 @@ public class FeedSearcher {
task.run();
}
try {
Set<Long> set = new HashSet<>();
for (int i = 0; i < tasks.size(); i++) {
FutureTask<List<FeedItem>> task = tasks.get(i);
List<FeedItem> items = task.get();
for (FeedItem item : items) {
if (result.isEmpty() || !isDuplicate(result, item)) {
if (!set.contains(item.getId())) { // to prevent duplicate results
result.add(new SearchResult(item, values[i], subtitles[i]));
set.add(item.getId());
}
}
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
@ -67,20 +71,4 @@ public class FeedSearcher {
Collections.sort(result, new SearchResultValueComparator());
return result;
}
/**
* Determines if the feed item is already in the search result list.
*
* @param result list of search results
* @param item feed item to validate
* @return true if the feed item is already in the results
*/
private static boolean isDuplicate(List<SearchResult> result, FeedItem item) {
for (SearchResult resultItem : result) {
if (resultItem.getComponent().getId() == item.getId()) {
return true;
}
}
return false;
}
}

View File

@ -42,12 +42,12 @@ import de.greenrobot.event.EventBus;
public class PodDBAdapter {
private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db";
private static final String DATABASE_NAME = "Antennapod.db";
/**
* Maximum number of arguments for IN-operator.
*/
public static final int IN_OPERATOR_MAXIMUM = 800;
private static final int IN_OPERATOR_MAXIMUM = 800;
/**
* Maximum number of entries per search request.
@ -57,6 +57,7 @@ public class PodDBAdapter {
// Key-constants
public static final String KEY_ID = "id";
public static final String KEY_TITLE = "title";
public static final String KEY_CUSTOM_TITLE = "custom_title";
public static final String KEY_NAME = "name";
public static final String KEY_LINK = "link";
public static final String KEY_DESCRIPTION = "description";
@ -109,14 +110,14 @@ public class PodDBAdapter {
public static final String KEY_EXCLUDE_FILTER = "exclude_filter";
// Table names
public static final String TABLE_NAME_FEEDS = "Feeds";
public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
public static final String TABLE_NAME_QUEUE = "Queue";
public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
public static final String TABLE_NAME_FAVORITES = "Favorites";
private static final String TABLE_NAME_FEEDS = "Feeds";
private static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
private static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
private static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
private static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
private static final String TABLE_NAME_QUEUE = "Queue";
private static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
private static final String TABLE_NAME_FAVORITES = "Favorites";
// SQL Statements for creating new tables
private static final String TABLE_PRIMARY_KEY = KEY_ID
@ -124,7 +125,7 @@ public class PodDBAdapter {
public static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
+ TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ " TEXT," + KEY_CUSTOM_TITLE + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT,"
+ KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR
@ -225,6 +226,7 @@ public class PodDBAdapter {
private static final String[] FEED_SEL_STD = {
TABLE_NAME_FEEDS + "." + KEY_ID,
TABLE_NAME_FEEDS + "." + KEY_TITLE,
TABLE_NAME_FEEDS + "." + KEY_CUSTOM_TITLE,
TABLE_NAME_FEEDS + "." + KEY_FILE_URL,
TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL,
TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED,
@ -363,7 +365,7 @@ public class PodDBAdapter {
*/
public long setFeed(Feed feed) {
ContentValues values = new ContentValues();
values.put(KEY_TITLE, feed.getTitle());
values.put(KEY_TITLE, feed.getFeedTitle());
values.put(KEY_LINK, feed.getLink());
values.put(KEY_DESCRIPTION, feed.getDescription());
values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
@ -854,6 +856,12 @@ public class PodDBAdapter {
db.execSQL(sql);
}
void setFeedCustomTitle(long feedId, String customTitle) {
ContentValues values = new ContentValues();
values.put(KEY_CUSTOM_TITLE, customTitle);
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)});
}
/**
* Inserts or updates a download status.
*/
@ -1436,15 +1444,24 @@ public class PodDBAdapter {
public final LongIntMap getFeedCounters(long... feedIds) {
int setting = UserPreferences.getFeedCounterSetting();
String whereRead;
if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM) {
whereRead = "(" + KEY_READ + "=" + FeedItem.NEW
+ " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
} else if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW) {
whereRead = KEY_READ + "=" + FeedItem.NEW;
} else if(setting == UserPreferences.FEED_COUNTER_SHOW_UNPLAYED) {
whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
} else { // NONE
return new LongIntMap(0);
switch(setting) {
case UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM:
whereRead = "(" + KEY_READ + "=" + FeedItem.NEW +
" OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")";
break;
case UserPreferences.FEED_COUNTER_SHOW_NEW:
whereRead = KEY_READ + "=" + FeedItem.NEW;
break;
case UserPreferences.FEED_COUNTER_SHOW_UNPLAYED:
whereRead = KEY_READ + "=" + FeedItem.UNPLAYED;
break;
case UserPreferences.FEED_COUNTER_SHOW_DOWNLOADED:
whereRead = KEY_DOWNLOADED + "=1";
break;
case UserPreferences.FEED_COUNTER_SHOW_NONE:
// deliberate fall-through
default: // NONE
return new LongIntMap(0);
}
// work around TextUtils.join wanting only boxed items
@ -1459,8 +1476,10 @@ public class PodDBAdapter {
builder.deleteCharAt(builder.length() - 1);
}
final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count "
final String query = "SELECT " + KEY_FEED + ", COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") AS count "
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
+ " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") "
+ " AND " + whereRead + " GROUP BY " + KEY_FEED;
@ -1617,7 +1636,7 @@ public class PodDBAdapter {
*/
private static class PodDBHelper extends SQLiteOpenHelper {
private static final int VERSION = 1050004;
private static final int VERSION = 1060200;
private Context context;
@ -1913,6 +1932,10 @@ public class PodDBAdapter {
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS
+" SET " + PodDBAdapter.KEY_LASTUPDATE + "=NULL");
}
if(oldVersion < 1060200) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_CUSTOM_TITLE + " TEXT");
}
EventBus.getDefault().post(ProgressEvent.end());
}

View File

@ -6,7 +6,6 @@ import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.syndication.namespace.NSContent;
import de.danoeh.antennapod.core.syndication.namespace.NSDublinCore;
@ -86,34 +85,28 @@ public class SyndHandler extends DefaultHandler {
state.defaultNamespaces.push(new NSAtom());
} else if (prefix.equals(NSAtom.NSTAG)) {
state.namespaces.put(uri, new NSAtom());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized Atom namespace");
Log.d(TAG, "Recognized Atom namespace");
}
} else if (uri.equals(NSContent.NSURI)
&& prefix.equals(NSContent.NSTAG)) {
state.namespaces.put(uri, new NSContent());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized Content namespace");
Log.d(TAG, "Recognized Content namespace");
} else if (uri.equals(NSITunes.NSURI)
&& prefix.equals(NSITunes.NSTAG)) {
state.namespaces.put(uri, new NSITunes());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized ITunes namespace");
Log.d(TAG, "Recognized ITunes namespace");
} else if (uri.equals(NSSimpleChapters.NSURI)
&& prefix.matches(NSSimpleChapters.NSTAG)) {
state.namespaces.put(uri, new NSSimpleChapters());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized SimpleChapters namespace");
Log.d(TAG, "Recognized SimpleChapters namespace");
} else if (uri.equals(NSMedia.NSURI)
&& prefix.equals(NSMedia.NSTAG)) {
state.namespaces.put(uri, new NSMedia());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized media namespace");
Log.d(TAG, "Recognized media namespace");
} else if (uri.equals(NSDublinCore.NSURI)
&& prefix.equals(NSDublinCore.NSTAG)) {
state.namespaces.put(uri, new NSDublinCore());
if (BuildConfig.DEBUG)
Log.d(TAG, "Recognized DublinCore namespace");
Log.d(TAG, "Recognized DublinCore namespace");
}
}
}

View File

@ -26,6 +26,11 @@ public class NSMedia extends Namespace {
private static final String MIME_TYPE = "type";
private static final String DURATION = "duration";
private static final String DEFAULT = "isDefault";
private static final String MEDIUM = "medium";
private static final String MEDIUM_IMAGE = "image";
private static final String MEDIUM_AUDIO = "audio";
private static final String MEDIUM_VIDEO = "video";
private static final String IMAGE = "thumbnail";
private static final String IMAGE_URL = "url";
@ -40,20 +45,31 @@ public class NSMedia extends Namespace {
String url = attributes.getValue(DOWNLOAD_URL);
String type = attributes.getValue(MIME_TYPE);
String defaultStr = attributes.getValue(DEFAULT);
boolean validType;
String medium = attributes.getValue(MEDIUM);
boolean validTypeMedia = false;
boolean validTypeImage = false;
boolean isDefault = "true".equals(defaultStr);
if (SyndTypeUtils.enclosureTypeValid(type)) {
validType = true;
if (MEDIUM_AUDIO.equals(medium) || MEDIUM_VIDEO.equals(medium)) {
validTypeMedia = true;
} else if (MEDIUM_IMAGE.equals(medium)) {
validTypeImage = true;
} else {
type = SyndTypeUtils.getValidMimeTypeFromUrl(url);
validType = type != null;
if (type == null) {
type = SyndTypeUtils.getMimeTypeFromUrl(url);
}
if (SyndTypeUtils.enclosureTypeValid(type)) {
validTypeMedia = true;
} else if (SyndTypeUtils.imageTypeValid(type)) {
validTypeImage = true;
}
}
if (state.getCurrentItem() != null &&
(state.getCurrentItem().getMedia() == null || isDefault) &&
url != null && validType) {
url != null && validTypeMedia) {
long size = 0;
String sizeStr = attributes.getValue(SIZE);
try {
@ -77,6 +93,12 @@ public class NSMedia extends Namespace {
media.setDuration(durationMs);
}
state.getCurrentItem().setMedia(media);
} else if (state.getCurrentItem() != null && url != null && validTypeImage) {
FeedImage image = new FeedImage();
image.setDownload_url(url);
image.setOwner(state.getCurrentItem());
state.getCurrentItem().setImage(image);
}
} else if (IMAGE.equals(localName)) {
String url = attributes.getValue(IMAGE_URL);

View File

@ -20,25 +20,27 @@ import de.danoeh.antennapod.core.util.DateUtils;
*
*/
public class NSRSS20 extends Namespace {
private static final String TAG = "NSRSS20";
public static final String NSTAG = "rss";
public static final String NSURI = "";
public static final String CHANNEL = "channel";
private static final String TAG = "NSRSS20";
private static final String NSTAG = "rss";
private static final String NSURI = "";
public static final String CHANNEL = "channel";
public static final String ITEM = "item";
public static final String GUID = "guid";
public static final String TITLE = "title";
public static final String LINK = "link";
public static final String DESCR = "description";
public static final String PUBDATE = "pubDate";
public static final String ENCLOSURE = "enclosure";
public static final String IMAGE = "image";
public static final String URL = "url";
public static final String LANGUAGE = "language";
private static final String GUID = "guid";
private static final String TITLE = "title";
private static final String LINK = "link";
private static final String DESCR = "description";
private static final String PUBDATE = "pubDate";
private static final String ENCLOSURE = "enclosure";
private static final String IMAGE = "image";
private static final String URL = "url";
private static final String LANGUAGE = "language";
public static final String ENC_URL = "url";
public static final String ENC_LEN = "length";
public static final String ENC_TYPE = "type";
private static final String ENC_URL = "url";
private static final String ENC_LEN = "length";
private static final String ENC_TYPE = "type";
@Override
public SyndElement handleElementStart(String localName, HandlerState state,
@ -51,15 +53,16 @@ public class NSRSS20 extends Namespace {
} else if (ENCLOSURE.equals(localName)) {
String type = attributes.getValue(ENC_TYPE);
String url = attributes.getValue(ENC_URL);
boolean validType;
if(SyndTypeUtils.enclosureTypeValid(type)) {
validType = true;
} else {
type = type = SyndTypeUtils.getValidMimeTypeFromUrl(url);
validType = type != null;
}
if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null &&
validType) {
boolean validType = SyndTypeUtils.enclosureTypeValid(type);
if(!validType) {
type = SyndTypeUtils.getMimeTypeFromUrl(url);
validType = SyndTypeUtils.enclosureTypeValid(type);
}
boolean validUrl = !TextUtils.isEmpty(url);
if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null &&
validType && validUrl) {
long size = 0;
try {
size = Long.parseLong(attributes.getValue(ENC_LEN));
@ -70,8 +73,8 @@ public class NSRSS20 extends Namespace {
} catch (NumberFormatException e) {
Log.d(TAG, "Length attribute could not be parsed.");
}
state.getCurrentItem().setMedia(
new FeedMedia(state.getCurrentItem(), url, size, type));
FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type);
state.getCurrentItem().setMedia(media);
}
} else if (IMAGE.equals(localName)) {

View File

@ -30,6 +30,7 @@ public class NSAtom extends Namespace {
private static final String AUTHOR = "author";
private static final String AUTHOR_NAME = "name";
private static final String CONTENT = "content";
private static final String SUMMARY = "summary";
private static final String IMAGE_LOGO = "logo";
private static final String IMAGE_ICON = "icon";
private static final String SUBTITLE = "subtitle";
@ -44,6 +45,7 @@ public class NSAtom extends Namespace {
private static final String LINK_LENGTH = "length";
// rel-values
private static final String LINK_REL_ALTERNATE = "alternate";
private static final String LINK_REL_ARCHIVES = "archives";
private static final String LINK_REL_ENCLOSURE = "enclosure";
private static final String LINK_REL_PAYMENT = "payment";
private static final String LINK_REL_RELATED = "related";
@ -59,8 +61,8 @@ public class NSAtom extends Namespace {
/**
* Regexp to test whether an Element is a Text Element.
*/
private static final String isText = TITLE + "|" + CONTENT + "|" + "|"
+ SUBTITLE;
private static final String isText = TITLE + "|" + CONTENT + "|"
+ SUBTITLE + "|" + SUMMARY;
public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL;
public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM;
@ -93,14 +95,12 @@ public class NSAtom extends Namespace {
Log.d(TAG, "Length attribute could not be parsed.");
}
String type = attributes.getValue(LINK_TYPE);
boolean validType;
if(SyndTypeUtils.enclosureTypeValid(type)) {
validType = true;
} else {
type = SyndTypeUtils.getValidMimeTypeFromUrl(href);
validType = type != null;
if (type == null) {
type = SyndTypeUtils.getMimeTypeFromUrl(href);
}
if (validType) {
if(SyndTypeUtils.enclosureTypeValid(type)) {
FeedItem currItem = state.getCurrentItem();
if(currItem != null && !currItem.hasMedia()) {
currItem.setMedia(new FeedMedia(currItem, href, size, type));
@ -129,6 +129,17 @@ public class NSAtom extends Namespace {
}
state.addAlternateFeedUrl(title, href);
}
} else if (LINK_REL_ARCHIVES.equals(rel) && state.getFeed() != null) {
String type = attributes.getValue(LINK_TYPE);
if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) {
String title = attributes.getValue(LINK_TITLE);
if (TextUtils.isEmpty(title)) {
title = href;
}
state.addAlternateFeedUrl(title, href);
} else if (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)) {
//A Link such as to a directory such as iTunes
}
} else if (LINK_REL_PAYMENT.equals(rel) && state.getFeed() != null) {
state.getFeed().setPaymentLink(href);
} else if (LINK_REL_NEXT.equals(rel) && state.getFeed() != null) {
@ -191,6 +202,9 @@ public class NSAtom extends Namespace {
} else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null &&
state.getCurrentItem() != null) {
state.getCurrentItem().setDescription(textElement.getProcessedContent());
} else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null &&
state.getCurrentItem() != null && state.getCurrentItem().getDescription() == null) {
state.getCurrentItem().setDescription(textElement.getProcessedContent());
} else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null &&
state.getCurrentItem().getPubDate() == null) {
state.getCurrentItem().setPubDate(DateUtils.parse(content));
@ -200,9 +214,13 @@ public class NSAtom extends Namespace {
state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
} else if (IMAGE_ICON.equals(top) && state.getFeed() != null) {
state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));
} else if (AUTHOR.equals(second) && state.getFeed() != null) {
if (AUTHOR_NAME.equals(top)) {
} else if (AUTHOR_NAME.equals(top) && AUTHOR.equals(second) &&
state.getFeed() != null && state.getCurrentItem() == null) {
String currentName = state.getFeed().getAuthor();
if (currentName == null) {
state.getFeed().setAuthor(content);
} else {
state.getFeed().setAuthor(currentName + ", " + content);
}
}
}

View File

@ -1,13 +1,22 @@
package de.danoeh.antennapod.core.syndication.util;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import org.apache.commons.io.FilenameUtils;
import java.util.Arrays;
/** Utility class for handling MIME-Types of enclosures */
public class SyndTypeUtils {
private static final String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*"
+ "|" + "application/ogg";
private static final String VALID_MEDIA_MIMETYPE = TextUtils.join("|", Arrays.asList(
"audio/.*",
"video/.*",
"application/ogg",
"application/octet-stream"));
private static final String VALID_IMAGE_MIMETYPE = "image/.*";
private SyndTypeUtils() {
@ -17,9 +26,17 @@ public class SyndTypeUtils {
if (type == null) {
return false;
} else {
return type.matches(VALID_MIMETYPE);
return type.matches(VALID_MEDIA_MIMETYPE);
}
}
public static boolean imageTypeValid(String type) {
if (type == null) {
return false;
} else {
return type.matches(VALID_IMAGE_MIMETYPE);
}
}
/**
* Should be used if mime-type of enclosure tag is not supported. This
@ -27,15 +44,27 @@ public class SyndTypeUtils {
* the type is not supported, this method will return null.
*/
public static String getValidMimeTypeFromUrl(String url) {
if (url != null) {
String extension = FilenameUtils.getExtension(url);
if (extension != null) {
String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (type != null && enclosureTypeValid(type)) {
return type;
}
}
String type = getMimeTypeFromUrl(url);
if (enclosureTypeValid(type)) {
return type;
} else {
return null;
}
return null;
}
/**
* Should be used if mime-type of enclosure tag is not supported. This
* method will return the mime-type of the file extension.
*/
public static String getMimeTypeFromUrl(String url) {
if (url == null) {
return null;
}
String extension = FilenameUtils.getExtension(url);
if (extension == null) {
return null;
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
}

View File

@ -8,11 +8,6 @@ import android.net.wifi.WifiManager;
import android.support.v4.net.ConnectivityManagerCompat;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
@ -22,6 +17,9 @@ import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.DBWriter;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;

View File

@ -1,14 +1,27 @@
package de.danoeh.antennapod.core.util.comparator;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.SearchResult;
import java.util.Comparator;
public class SearchResultValueComparator implements Comparator<SearchResult> {
/**
* Compare items based, first, on where they were found (ie. title, chapters, or show notes).
* If they were found in the same section, then compare based on the title, in lexicographic
* order. This is still not ideal since, for example, "#12 Example A" would be considered
* before "#8 Example B" due to the fact that "8" has a larger unicode value than "1"
*/
@Override
public int compare(SearchResult lhs, SearchResult rhs) {
return rhs.getValue() - lhs.getValue();
int value = rhs.getValue() - lhs.getValue();
if (value == 0 && lhs.getComponent() instanceof FeedItem && rhs.getComponent() instanceof FeedItem) {
String lhsTitle = ((FeedItem) lhs.getComponent()).getTitle();
String rhsTitle = ((FeedItem) rhs.getComponent()).getTitle();
return lhsTitle.compareTo(rhsTitle);
}
return value;
}
}

View File

@ -216,7 +216,7 @@ public abstract class PlaybackController {
Intent serviceIntent = new Intent(activity, PlaybackService.class);
serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false);
serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false);
serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, true);
boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {

View File

@ -0,0 +1,89 @@
package de.danoeh.antennapod.core.util.syndication;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
/**
* This class is based on <code>HtmlToPlainText</code> from jsoup's examples package.
*
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
* scrape.
* <p>
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
* </p>
* <p>
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:</p>
* <p><code>java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]</code></p>
* where <i>url</i> is the URL to fetch, and <i>selector</i> is an optional CSS selector.
*
* @author Jonathan Hedley, jonathan@hedley.net
* @author AntennaPod open source community
*/
public class HtmlToPlainText {
/**
* Format an Element to plain-text
* @param element the root element to format
* @return formatted text
*/
public String getPlainText(Element element) {
FormattingVisitor formatter = new FormattingVisitor();
NodeTraversor traversor = new NodeTraversor(formatter);
traversor.traverse(element); // walk the DOM, and call .head() and .tail() for each node
return formatter.toString();
}
// the formatting rules, implemented in a breadth-first DOM traverse
private class FormattingVisitor implements NodeVisitor {
private StringBuilder accum = new StringBuilder(); // holds the accumulated text
// hit when the node is first seen
public void head(Node node, int depth) {
String name = node.nodeName();
if (node instanceof TextNode) {
append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
}
else if (name.equals("li")) {
append("\n * ");
}
else if (name.equals("dt")) {
append(" ");
}
else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr")) {
append("\n");
}
}
// hit when all of the node's children (if any) have been visited
public void tail(Node node, int depth) {
String name = node.nodeName();
if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) {
append("\n");
} else if (name.equals("a")) {
append(String.format(" <%s>", node.absUrl("href")));
}
}
// appends text to the string builder with a simple word wrap method
private void append(String text) {
if (text.equals(" ") &&
(accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n"))) {
return; // don't accumulate long runs of empty spaces
}
accum.append(text);
}
@Override
public String toString() {
return accum.toString();
}
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/overlay_dark"/>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_launcher"/>
</item>
</layer-list>

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