summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle84
-rw-r--r--app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java218
-rw-r--r--app/src/free/java/de/danoeh/antennapod/activity/MainActivity.java773
-rw-r--r--app/src/free/java/de/danoeh/antennapod/activity/MediaplayerActivity.java876
-rw-r--r--app/src/free/java/de/danoeh/antennapod/fragment/ItemFragment.java592
-rw-r--r--app/src/free/java/de/danoeh/antennapod/preferences/PreferenceController.java951
-rw-r--r--app/src/main/AndroidManifest.xml8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java8
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java9
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java14
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java15
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java15
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java14
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java7
-rw-r--r--app/src/main/res/menu/queue.xml37
-rw-r--r--app/src/main/res/values/strings.xml4
-rw-r--r--app/src/play/AndroidManifest.xml11
-rw-r--r--app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java (renamed from app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java)0
-rw-r--r--app/src/play/java/de/danoeh/antennapod/activity/MainActivity.java (renamed from app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java)2
-rw-r--r--app/src/play/java/de/danoeh/antennapod/activity/MediaplayerActivity.java (renamed from app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java)0
-rw-r--r--app/src/play/java/de/danoeh/antennapod/fragment/ItemFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java)4
-rw-r--r--app/src/play/java/de/danoeh/antennapod/preferences/PreferenceController.java (renamed from app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java)0
-rw-r--r--core/build.gradle12
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java48
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/feed/FeedMedia.java567
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java1773
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java592
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/util/playback/Playable.java245
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java17
-rw-r--r--core/src/main/res/values-id/strings.xml22
-rw-r--r--core/src/main/res/values-it-rIT/strings.xml72
-rw-r--r--core/src/main/res/values-ko/strings.xml60
-rw-r--r--core/src/main/res/values-pt-rBR/strings.xml200
-rw-r--r--core/src/main/res/values-ru/strings.xml167
-rw-r--r--core/src/main/res/values-uk-rUA/strings.xml51
-rw-r--r--core/src/main/res/values/strings.xml4
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java (renamed from core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java (renamed from core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/feed/FeedMedia.java (renamed from core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java (renamed from core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java (renamed from core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java)0
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/util/playback/Playable.java (renamed from core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java)0
59 files changed, 7340 insertions, 189 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 9a3f57caa..43efc8dc2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,44 +9,6 @@ repositories {
mavenCentral()
}
-dependencies {
- compile project(":core")
- compile "com.android.support:support-v4:$supportVersion"
- compile "com.android.support:appcompat-v7:$supportVersion"
- compile "com.android.support:design:$supportVersion"
- compile "com.android.support:gridlayout-v7:$supportVersion"
- compile "com.android.support:percent:$supportVersion"
- compile "com.android.support:recyclerview-v7:$supportVersion"
- compile "org.apache.commons:commons-lang3:$commonslangVersion"
- compile("org.shredzone.flattr4j:flattr4j-core:$flattr4jVersion") {
- exclude group: "org.json", module: "json"
- }
- 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.okio:okio:$okioVersion"
- compile "de.greenrobot:eventbus:$eventbusVersion"
- compile "io.reactivex:rxandroid:$rxAndroidVersion"
- compile "io.reactivex:rxjava:$rxJavaVersion"
- // And ProGuard rules for RxJava!
- compile "com.artemzin.rxjava:proguard-rules:$rxJavaRulesVersion"
- compile "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion"
- compile "com.joanzapata.iconify:android-iconify-material:$iconifyVersion"
- compile("com.github.afollestad.material-dialogs:commons:$materialDialogsVersion") {
- transitive = true
- }
- compile "com.yqritc:recyclerview-flexibledivider:$recyclerviewFlexibledividerVersion"
- compile("com.githang:viewpagerindicator:2.5@aar") {
- exclude module: "support-v4"
- }
-
- compile "com.github.shts:TriangleLabelView:$triangleLabelViewVersion"
-
- compile "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
-}
-
def getMyVersionName() {
def parsedManifestXml = (new XmlSlurper())
.parse("${projectDir}/src/main/AndroidManifest.xml")
@@ -138,6 +100,52 @@ android {
aaptOptions {
additionalParameters "--no-version-vectors"
}
+
+ productFlavors {
+ free {
+ }
+ play {
+ }
+ }
+}
+
+dependencies {
+ freeCompile project(path: ":core", configuration: "freeRelease")
+ playCompile project(path: ":core", configuration: "playRelease")
+ compile "com.android.support:support-v4:$supportVersion"
+ compile "com.android.support:appcompat-v7:$supportVersion"
+ compile "com.android.support:design:$supportVersion"
+ compile "com.android.support:gridlayout-v7:$supportVersion"
+ compile "com.android.support:percent:$supportVersion"
+ compile "com.android.support:recyclerview-v7:$supportVersion"
+ compile "org.apache.commons:commons-lang3:$commonslangVersion"
+ compile("org.shredzone.flattr4j:flattr4j-core:$flattr4jVersion") {
+ exclude group: "org.json", module: "json"
+ }
+ 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.okio:okio:$okioVersion"
+ compile "de.greenrobot:eventbus:$eventbusVersion"
+ compile "io.reactivex:rxandroid:$rxAndroidVersion"
+ compile "io.reactivex:rxjava:$rxJavaVersion"
+ // And ProGuard rules for RxJava!
+ compile "com.artemzin.rxjava:proguard-rules:$rxJavaRulesVersion"
+ compile "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion"
+ compile "com.joanzapata.iconify:android-iconify-material:$iconifyVersion"
+ compile("com.github.afollestad.material-dialogs:commons:$materialDialogsVersion") {
+ transitive = true
+ }
+ compile "com.yqritc:recyclerview-flexibledivider:$recyclerviewFlexibledividerVersion"
+ compile("com.githang:viewpagerindicator:2.5@aar") {
+ exclude module: "support-v4"
+ }
+
+ compile "com.github.shts:TriangleLabelView:$triangleLabelViewVersion"
+
+ compile "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
}
play {
diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
new file mode 100644
index 000000000..d82b199db
--- /dev/null
+++ b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
@@ -0,0 +1,218 @@
+package de.danoeh.antennapod.activity;
+
+import android.support.v7.app.AppCompatActivity;
+
+/**
+ * Activity that allows for showing the MediaRouter button whenever there's a cast device in the
+ * network.
+ */
+public abstract class CastEnabledActivity extends AppCompatActivity {
+// implements SharedPreferences.OnSharedPreferenceChangeListener {
+ public static final String TAG = "CastEnabledActivity";
+
+// protected CastManager castManager;
+// protected SwitchableMediaRouteActionProvider mediaRouteActionProvider;
+// private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager();
+//
+// @Override
+// protected void onCreate(Bundle savedInstanceState) {
+// super.onCreate(savedInstanceState);
+//
+// PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).
+// registerOnSharedPreferenceChangeListener(this);
+//
+// castManager = CastManager.getInstance();
+// castManager.addCastConsumer(castConsumer);
+// castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
+// onCastConnectionChanged(castManager.isConnected());
+// }
+//
+// @Override
+// protected void onDestroy() {
+// PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+// .unregisterOnSharedPreferenceChangeListener(this);
+// castManager.removeCastConsumer(castConsumer);
+// super.onDestroy();
+// }
+//
+// @Override
+// @CallSuper
+// public boolean onCreateOptionsMenu(Menu menu) {
+// super.onCreateOptionsMenu(menu);
+// getMenuInflater().inflate(R.menu.cast_enabled, menu);
+// castButtonVisibilityManager.setMenu(menu);
+// return true;
+// }
+//
+// @Override
+// @CallSuper
+// public boolean onPrepareOptionsMenu(Menu menu) {
+// super.onPrepareOptionsMenu(menu);
+// mediaRouteActionProvider = castManager
+// .addMediaRouterButton(menu.findItem(R.id.media_route_menu_item));
+// mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable());
+// return true;
+// }
+//
+// @Override
+// protected void onResume() {
+// super.onResume();
+// castButtonVisibilityManager.setResumed(true);
+// }
+//
+// @Override
+// protected void onPause() {
+// super.onPause();
+// castButtonVisibilityManager.setResumed(false);
+// }
+//
+// @Override
+// public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+// if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
+// boolean newValue = UserPreferences.isCastEnabled();
+// Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue);
+// castButtonVisibilityManager.setPrefEnabled(newValue);
+// // PlaybackService has its own listener, so if it's active we don't have to take action here.
+// if (!newValue && !PlaybackService.isRunning) {
+// CastManager.getInstance().disconnect();
+// }
+// }
+// }
+//
+// CastConsumer castConsumer = new DefaultCastConsumer() {
+// @Override
+// public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
+// onCastConnectionChanged(true);
+// }
+//
+// @Override
+// public void onDisconnected() {
+// onCastConnectionChanged(false);
+// }
+// };
+//
+// private void onCastConnectionChanged(boolean connected) {
+// if (connected) {
+// castButtonVisibilityManager.onConnected();
+// setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
+// } else {
+// castButtonVisibilityManager.onDisconnected();
+// setVolumeControlStream(AudioManager.STREAM_MUSIC);
+// }
+// }
+//
+// /**
+// * Should be called by any activity or fragment for which the cast button should be shown.
+// *
+// * @param showAsAction refer to {@link MenuItem#setShowAsAction(int)}
+// */
+// public final void requestCastButton(int showAsAction) {
+// castButtonVisibilityManager.requestCastButton(showAsAction);
+// }
+//
+// private class CastButtonVisibilityManager {
+// private volatile boolean prefEnabled = false;
+// private volatile boolean viewRequested = false;
+// private volatile boolean resumed = false;
+// private volatile boolean connected = false;
+// private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
+// private Menu menu;
+//
+// public synchronized void setPrefEnabled(boolean newValue) {
+// if (prefEnabled != newValue && resumed && (viewRequested || connected)) {
+// if (newValue) {
+// castManager.incrementUiCounter();
+// } else {
+// castManager.decrementUiCounter();
+// }
+// }
+// prefEnabled = newValue;
+// if (mediaRouteActionProvider != null) {
+// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
+// }
+// }
+//
+// public synchronized void setResumed(boolean newValue) {
+// if (resumed == newValue) {
+// Log.e(TAG, "resumed should never change to the same value");
+// return;
+// }
+// resumed = newValue;
+// if (prefEnabled && (viewRequested || connected)) {
+// if (resumed) {
+// castManager.incrementUiCounter();
+// } else {
+// castManager.decrementUiCounter();
+// }
+// }
+// }
+//
+// public synchronized void setViewRequested(boolean newValue) {
+// if (viewRequested != newValue && resumed && prefEnabled && !connected) {
+// if (newValue) {
+// castManager.incrementUiCounter();
+// } else {
+// castManager.decrementUiCounter();
+// }
+// }
+// viewRequested = newValue;
+// if (mediaRouteActionProvider != null) {
+// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
+// }
+// }
+//
+// public synchronized void setConnected(boolean newValue) {
+// if (connected != newValue && resumed && prefEnabled && !prefEnabled) {
+// if (newValue) {
+// castManager.incrementUiCounter();
+// } else {
+// castManager.decrementUiCounter();
+// }
+// }
+// connected = newValue;
+// if (mediaRouteActionProvider != null) {
+// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
+// }
+// }
+//
+// public synchronized boolean shouldEnable() {
+// return prefEnabled && viewRequested;
+// }
+//
+// public void setMenu(Menu menu) {
+// setViewRequested(false);
+// showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
+// this.menu = menu;
+// setShowAsAction();
+// }
+//
+// public void requestCastButton(int showAsAction) {
+// setViewRequested(true);
+// this.showAsAction = showAsAction;
+// setShowAsAction();
+// }
+//
+// public void onConnected() {
+// setConnected(true);
+// setShowAsAction();
+// }
+//
+// public void onDisconnected() {
+// setConnected(false);
+// setShowAsAction();
+// }
+//
+// private void setShowAsAction() {
+// if (menu == null) {
+// Log.d(TAG, "setShowAsAction() without a menu");
+// return;
+// }
+// MenuItem item = menu.findItem(R.id.media_route_menu_item);
+// if (item == null) {
+// Log.e(TAG, "setShowAsAction(), but cast button not inflated");
+// return;
+// }
+// MenuItemCompat.setShowAsAction(item, connected? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction);
+// }
+// }
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/MainActivity.java
new file mode 100644
index 000000000..c4771a583
--- /dev/null
+++ b/app/src/free/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -0,0 +1,773 @@
+package de.danoeh.antennapod.activity;
+
+import android.annotation.TargetApi;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.database.DataSetObserver;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.bumptech.glide.Glide;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.Validate;
+
+import java.util.List;
+
+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.ProgressEvent;
+import de.danoeh.antennapod.core.event.QueueEvent;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.dialog.RatingDialog;
+import de.danoeh.antennapod.fragment.AddFeedFragment;
+import de.danoeh.antennapod.fragment.DownloadsFragment;
+import de.danoeh.antennapod.fragment.EpisodesFragment;
+import de.danoeh.antennapod.fragment.ExternalPlayerFragment;
+import de.danoeh.antennapod.fragment.ItemlistFragment;
+import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
+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;
+import rx.schedulers.Schedulers;
+
+/**
+ * The activity that is shown when the user launches the app.
+ */
+public class MainActivity extends CastEnabledActivity implements NavDrawerActivity {
+
+ private static final String TAG = "MainActivity";
+
+ private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE
+ | EventDistributor.UNREAD_ITEMS_UPDATE;
+
+ public static final String PREF_NAME = "MainActivityPrefs";
+ public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch";
+ public static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag";
+
+ public static final String EXTRA_NAV_TYPE = "nav_type";
+ public static final String EXTRA_NAV_INDEX = "nav_index";
+ public static final String EXTRA_FRAGMENT_TAG = "fragment_tag";
+ public static final String EXTRA_FRAGMENT_ARGS = "fragment_args";
+ public static final String EXTRA_FEED_ID = "fragment_feed_id";
+
+ public static final String SAVE_BACKSTACK_COUNT = "backstackCount";
+ public static final String SAVE_TITLE = "title";
+
+ public static final String[] NAV_DRAWER_TAGS = {
+ QueueFragment.TAG,
+ EpisodesFragment.TAG,
+ SubscriptionFragment.TAG,
+ DownloadsFragment.TAG,
+ PlaybackHistoryFragment.TAG,
+ AddFeedFragment.TAG,
+ NavListAdapter.SUBSCRIPTION_LIST_TAG
+ };
+
+ private Toolbar toolbar;
+ private ExternalPlayerFragment externalPlayerFragment;
+ private DrawerLayout drawerLayout;
+
+ private View navDrawer;
+ private ListView navList;
+ private NavListAdapter navAdapter;
+ private int mPosition = -1;
+
+ private ActionBarDrawerToggle drawerToggle;
+
+ private CharSequence currentTitle;
+
+ private ProgressDialog pd;
+
+ private Subscription subscription;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setTheme(UserPreferences.getNoTitleTheme());
+ super.onCreate(savedInstanceState);
+ StorageUtils.checkStorageAvailability(this);
+ setContentView(R.layout.main);
+
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ findViewById(R.id.shadow).setVisibility(View.GONE);
+ int elevation = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4,
+ getResources().getDisplayMetrics());
+ getSupportActionBar().setElevation(elevation);
+ }
+
+ currentTitle = getTitle();
+
+ drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ navList = (ListView) findViewById(R.id.nav_list);
+ navDrawer = findViewById(R.id.nav_layout);
+
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close);
+ if (savedInstanceState != null) {
+ int backstackCount = savedInstanceState.getInt(SAVE_BACKSTACK_COUNT, 0);
+ drawerToggle.setDrawerIndicatorEnabled(backstackCount == 0);
+ }
+ drawerLayout.setDrawerListener(drawerToggle);
+
+ final FragmentManager fm = getSupportFragmentManager();
+
+ fm.addOnBackStackChangedListener(() -> {
+ drawerToggle.setDrawerIndicatorEnabled(fm.getBackStackEntryCount() == 0);
+ });
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ navAdapter = new NavListAdapter(itemAccess, this);
+ navList.setAdapter(navAdapter);
+ navList.setOnItemClickListener(navListClickListener);
+ navList.setOnItemLongClickListener(newListLongClickListener);
+ registerForContextMenu(navList);
+
+ navAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ selectedNavListIndex = getSelectedNavListIndex();
+ }
+ });
+
+ findViewById(R.id.nav_settings).setOnClickListener(v -> {
+ drawerLayout.closeDrawer(navDrawer);
+ startActivity(new Intent(MainActivity.this, PreferenceController.getPreferenceActivity()));
+ });
+
+ FragmentTransaction transaction = fm.beginTransaction();
+
+ Fragment mainFragment = fm.findFragmentByTag("main");
+ if (mainFragment != null) {
+ transaction.replace(R.id.main_view, mainFragment);
+ } else {
+ String lastFragment = getLastNavFragment();
+ if (ArrayUtils.contains(NAV_DRAWER_TAGS, lastFragment)) {
+ loadFragment(lastFragment, null);
+ } else {
+ try {
+ loadFeedFragmentById(Integer.parseInt(lastFragment), null);
+ } catch (NumberFormatException e) {
+ // it's not a number, this happens if we removed
+ // a label from the NAV_DRAWER_TAGS
+ // give them a nice default...
+ loadFragment(QueueFragment.TAG, null);
+ }
+ }
+ }
+ externalPlayerFragment = new ExternalPlayerFragment();
+ transaction.replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG);
+ transaction.commit();
+
+ checkFirstLaunch();
+ }
+
+ private void saveLastNavFragment(String tag) {
+ Log.d(TAG, "saveLastNavFragment(tag: " + tag +")");
+ SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ if(tag != null) {
+ edit.putString(PREF_LAST_FRAGMENT_TAG, tag);
+ } else {
+ edit.remove(PREF_LAST_FRAGMENT_TAG);
+ }
+ edit.commit();
+ }
+
+ private String getLastNavFragment() {
+ SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, QueueFragment.TAG);
+ Log.d(TAG, "getLastNavFragment() -> " + lastFragment);
+ return lastFragment;
+ }
+
+ private void checkFirstLaunch() {
+ SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) {
+ new Handler().postDelayed(() -> drawerLayout.openDrawer(navDrawer), 1500);
+
+ // for backward compatibility, we only change defaults for fresh installs
+ UserPreferences.setUpdateInterval(12);
+
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putBoolean(PREF_IS_FIRST_LAUNCH, false);
+ edit.commit();
+ }
+ }
+
+ public void showDrawerPreferencesDialog() {
+ final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems();
+ String[] navLabels = new String[NAV_DRAWER_TAGS.length];
+ final boolean[] checked = new boolean[NAV_DRAWER_TAGS.length];
+ for (int i = 0; i < NAV_DRAWER_TAGS.length; i++) {
+ String tag = NAV_DRAWER_TAGS[i];
+ navLabels[i] = navAdapter.getLabel(tag);
+ if (!hiddenDrawerItems.contains(tag)) {
+ checked[i] = true;
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
+ builder.setTitle(R.string.drawer_preferences);
+ builder.setMultiChoiceItems(navLabels, checked, (dialog, which, isChecked) -> {
+ if (isChecked) {
+ hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]);
+ } else {
+ hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]);
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.create().show();
+ }
+
+ public boolean isDrawerOpen() {
+ return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer);
+ }
+
+ public List<Feed> getFeeds() {
+ return (navDrawerData != null) ? navDrawerData.feeds : null;
+ }
+
+ public void loadFragment(int index, Bundle args) {
+ Log.d(TAG, "loadFragment(index: " + index + ", args: " + args + ")");
+ if (index < navAdapter.getSubscriptionOffset()) {
+ String tag = navAdapter.getTags().get(index);
+ loadFragment(tag, args);
+ } else {
+ int pos = index - navAdapter.getSubscriptionOffset();
+ loadFeedFragmentByPosition(pos, args);
+ }
+ }
+
+ public void loadFragment(String tag, Bundle args) {
+ Log.d(TAG, "loadFragment(tag: " + tag + ", args: " + args + ")");
+ Fragment fragment = null;
+ switch (tag) {
+ case QueueFragment.TAG:
+ fragment = new QueueFragment();
+ break;
+ case EpisodesFragment.TAG:
+ fragment = new EpisodesFragment();
+ break;
+ case DownloadsFragment.TAG:
+ fragment = new DownloadsFragment();
+ break;
+ case PlaybackHistoryFragment.TAG:
+ fragment = new PlaybackHistoryFragment();
+ break;
+ case AddFeedFragment.TAG:
+ fragment = new AddFeedFragment();
+ break;
+ case SubscriptionFragment.TAG:
+ SubscriptionFragment subscriptionFragment = new SubscriptionFragment();
+ fragment = subscriptionFragment;
+ break;
+ default:
+ // default to the queue
+ tag = QueueFragment.TAG;
+ fragment = new QueueFragment();
+ args = null;
+ break;
+ }
+ currentTitle = navAdapter.getLabel(tag);
+ getSupportActionBar().setTitle(currentTitle);
+ saveLastNavFragment(tag);
+ if (args != null) {
+ fragment.setArguments(args);
+ }
+ loadFragment(fragment);
+ }
+
+ private void loadFeedFragmentByPosition(int relPos, Bundle args) {
+ if(relPos < 0) {
+ return;
+ }
+ Feed feed = itemAccess.getItem(relPos);
+ loadFeedFragmentById(feed.getId(), args);
+ }
+
+ public void loadFeedFragmentById(long feedId, Bundle args) {
+ Fragment fragment = ItemlistFragment.newInstance(feedId);
+ if(args != null) {
+ fragment.setArguments(args);
+ }
+ saveLastNavFragment(String.valueOf(feedId));
+ currentTitle = "";
+ getSupportActionBar().setTitle(currentTitle);
+ loadFragment(fragment);
+ }
+
+ private void loadFragment(Fragment fragment) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ // clear back stack
+ for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+ fragmentManager.popBackStack();
+ }
+ FragmentTransaction t = fragmentManager.beginTransaction();
+ t.replace(R.id.main_view, fragment, "main");
+ fragmentManager.popBackStack();
+ // TODO: we have to allow state loss here
+ // since this function can get called from an AsyncTask which
+ // could be finishing after our app has already committed state
+ // and is about to get shutdown. What we *should* do is
+ // not commit anything in an AsyncTask, but that's a bigger
+ // change than we want now.
+ t.commitAllowingStateLoss();
+ if (navAdapter != null) {
+ navAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void loadChildFragment(Fragment fragment) {
+ Validate.notNull(fragment);
+ FragmentManager fm = getSupportFragmentManager();
+ fm.beginTransaction()
+ .replace(R.id.main_view, fragment, "main")
+ .addToBackStack(null)
+ .commit();
+ }
+
+ public void dismissChildFragment() {
+ getSupportFragmentManager().popBackStack();
+ }
+
+ private int getSelectedNavListIndex() {
+ String currentFragment = getLastNavFragment();
+ if(currentFragment == null) {
+ // should not happen, but better safe than sorry
+ return -1;
+ }
+ int tagIndex = navAdapter.getTags().indexOf(currentFragment);
+ if(tagIndex >= 0) {
+ return tagIndex;
+ } else if(ArrayUtils.contains(NAV_DRAWER_TAGS, currentFragment)) {
+ // the fragment was just hidden
+ return -1;
+ } else { // last fragment was not a list, but a feed
+ long feedId = Long.parseLong(currentFragment);
+ if (navDrawerData != null) {
+ List<Feed> feeds = navDrawerData.feeds;
+ for (int i = 0; i < feeds.size(); i++) {
+ if (feeds.get(i).getId() == feedId) {
+ return i + navAdapter.getSubscriptionOffset();
+ }
+ }
+ }
+ return -1;
+ }
+ }
+
+ private AdapterView.OnItemClickListener navListClickListener = new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ int viewType = parent.getAdapter().getItemViewType(position);
+ if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER && position != selectedNavListIndex) {
+ loadFragment(position, null);
+ }
+ drawerLayout.closeDrawer(navDrawer);
+ }
+ };
+
+ private AdapterView.OnItemLongClickListener newListLongClickListener = new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ if(position < navAdapter.getTags().size()) {
+ showDrawerPreferencesDialog();
+ return true;
+ } else {
+ mPosition = position;
+ return false;
+ }
+ }
+ };
+
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ drawerToggle.syncState();
+ if (savedInstanceState != null) {
+ currentTitle = savedInstanceState.getString(SAVE_TITLE);
+ if (!drawerLayout.isDrawerOpen(navDrawer)) {
+ getSupportActionBar().setTitle(currentTitle);
+ }
+ selectedNavListIndex = getSelectedNavListIndex();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(SAVE_TITLE, getSupportActionBar().getTitle().toString());
+ outState.putInt(SAVE_BACKSTACK_COUNT, getSupportFragmentManager().getBackStackEntryCount());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventDistributor.getInstance().register(contentUpdate);
+ EventBus.getDefault().register(this);
+ RatingDialog.init(this);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+
+ Intent intent = getIntent();
+ if (intent.hasExtra(EXTRA_FEED_ID) ||
+ (navDrawerData != null && intent.hasExtra(EXTRA_NAV_TYPE) &&
+ (intent.hasExtra(EXTRA_NAV_INDEX) || intent.hasExtra(EXTRA_FRAGMENT_TAG)))) {
+ handleNavIntent();
+ }
+ loadData();
+ RatingDialog.check();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ EventBus.getDefault().unregister(this);
+ if(subscription != null) {
+ subscription.unsubscribe();
+ }
+ if(pd != null) {
+ pd.dismiss();
+ }
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ Glide.get(this).trimMemory(level);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ Glide.get(this).clearMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ boolean retVal = super.onCreateOptionsMenu(menu);
+ switch (getLastNavFragment()) {
+ case QueueFragment.TAG:
+ case EpisodesFragment.TAG:
+// requestCastButton(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return retVal;
+ case DownloadsFragment.TAG:
+ case PlaybackHistoryFragment.TAG:
+ case AddFeedFragment.TAG:
+ case SubscriptionFragment.TAG:
+ return retVal;
+ default:
+// requestCastButton(MenuItem.SHOW_AS_ACTION_NEVER);
+ return retVal;
+ }
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ } else if (item.getItemId() == android.R.id.home) {
+ if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
+ dismissChildFragment();
+ }
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if(v.getId() != R.id.nav_list) {
+ return;
+ }
+ AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ int position = adapterInfo.position;
+ if(position < navAdapter.getSubscriptionOffset()) {
+ return;
+ }
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.nav_feed_context, menu);
+ Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
+ menu.setHeaderTitle(feed.getTitle());
+ // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
+ }
+
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ final int position = mPosition;
+ mPosition = -1; // reset
+ if(position < 0) {
+ return false;
+ }
+ Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
+ switch(item.getItemId()) {
+ case R.id.mark_all_seen_item:
+ DBWriter.markFeedSeen(feed.getId());
+ return true;
+ case R.id.mark_all_read_item:
+ DBWriter.markFeedRead(feed.getId());
+ return true;
+ case R.id.remove_item:
+ final FeedRemover remover = new FeedRemover(this, feed) {
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ if(getSelectedNavListIndex() == position) {
+ loadFragment(EpisodesFragment.TAG, null);
+ }
+ }
+ };
+ ConfirmationDialog conDialog = new ConfirmationDialog(this,
+ R.string.remove_feed_label,
+ R.string.feed_delete_confirmation_msg) {
+ @Override
+ public void onConfirmButtonPressed(
+ DialogInterface dialog) {
+ dialog.dismiss();
+ long mediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId();
+ if (mediaId > 0 &&
+ FeedItemUtil.indexOfItemWithMediaId(feed.getItems(), mediaId) >= 0) {
+ Log.d(TAG, "Currently playing episode is about to be deleted, skipping");
+ remover.skipOnCompletion = true;
+ int playerStatus = PlaybackPreferences.getCurrentPlayerStatus();
+ if(playerStatus == PlaybackPreferences.PLAYER_STATUS_PLAYING) {
+ sendBroadcast(new Intent(
+ PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE));
+ }
+ }
+ remover.executeAsync();
+ }
+ };
+ conDialog.createNewDialog().show();
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(isDrawerOpen()) {
+ drawerLayout.closeDrawer(navDrawer);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private DBReader.NavDrawerData navDrawerData;
+ private int selectedNavListIndex = 0;
+
+ private NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ if (navDrawerData != null) {
+ return navDrawerData.feeds.size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Feed getItem(int position) {
+ if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) {
+ return navDrawerData.feeds.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getSelectedItemIndex() {
+ return selectedNavListIndex;
+ }
+
+ @Override
+ public int getQueueSize() {
+ return (navDrawerData != null) ? navDrawerData.queueSize : 0;
+ }
+
+ @Override
+ public int getNumberOfNewItems() {
+ return (navDrawerData != null) ? navDrawerData.numNewItems : 0;
+ }
+
+ @Override
+ public int getNumberOfDownloadedItems() {
+ return (navDrawerData != null) ? navDrawerData.numDownloadedItems : 0;
+ }
+
+ @Override
+ public int getReclaimableItems() {
+ return (navDrawerData != null) ? navDrawerData.reclaimableSpace : 0;
+ }
+
+ @Override
+ public int getFeedCounter(long feedId) {
+ return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0;
+ }
+
+ @Override
+ public int getFeedCounterSum() {
+ if(navDrawerData == null) {
+ return 0;
+ }
+ int sum = 0;
+ for(int counter : navDrawerData.feedCounters.values()) {
+ sum += counter;
+ }
+ return sum;
+ }
+
+ };
+
+ private void loadData() {
+ subscription = Observable.fromCallable(DBReader::getNavDrawerData)
+ .subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ boolean handleIntent = (navDrawerData == null);
+
+ navDrawerData = result;
+ navAdapter.notifyDataSetChanged();
+
+ if (handleIntent) {
+ handleNavIntent();
+ }
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ });
+ }
+
+ public void onEvent(QueueEvent event) {
+ Log.d(TAG, "onEvent(" + event + ")");
+ // we are only interested in the number of queue items, not download status or position
+ if(event.action == QueueEvent.Action.DELETED_MEDIA ||
+ event.action == QueueEvent.Action.SORTED ||
+ event.action == QueueEvent.Action.MOVED) {
+ return;
+ }
+ loadData();
+ }
+
+ public void onEventMainThread(ProgressEvent event) {
+ Log.d(TAG, "onEvent(" + event + ")");
+ switch(event.action) {
+ case START:
+ pd = new ProgressDialog(this);
+ pd.setMessage(event.message);
+ pd.setIndeterminate(true);
+ pd.setCancelable(false);
+ pd.show();
+ break;
+ case END:
+ if(pd != null) {
+ pd.dismiss();
+ }
+ break;
+ }
+ }
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EVENTS & arg) != 0) {
+ Log.d(TAG, "Received contentUpdate Intent.");
+ loadData();
+ }
+ }
+ };
+
+ private void handleNavIntent() {
+ Log.d(TAG, "handleNavIntent()");
+ Intent intent = getIntent();
+ if (intent.hasExtra(EXTRA_FEED_ID) ||
+ (intent.hasExtra(EXTRA_NAV_TYPE) &&
+ (intent.hasExtra(EXTRA_NAV_INDEX) || intent.hasExtra(EXTRA_FRAGMENT_TAG)))) {
+ int index = intent.getIntExtra(EXTRA_NAV_INDEX, -1);
+ String tag = intent.getStringExtra(EXTRA_FRAGMENT_TAG);
+ Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS);
+ long feedId = intent.getLongExtra(EXTRA_FEED_ID, 0);
+ if (index >= 0) {
+ loadFragment(index, args);
+ } else if (tag != null) {
+ loadFragment(tag, args);
+ } else if(feedId > 0) {
+ loadFeedFragmentById(feedId, args);
+ }
+ }
+ setIntent(new Intent(MainActivity.this, MainActivity.class)); // to avoid handling the intent twice when the configuration changes
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
new file mode 100644
index 000000000..0d6e5504a
--- /dev/null
+++ b/app/src/free/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -0,0 +1,876 @@
+package de.danoeh.antennapod.activity;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.bumptech.glide.Glide;
+import com.joanzapata.iconify.IconDrawable;
+import com.joanzapata.iconify.fonts.FontAwesomeIcons;
+
+import java.util.Locale;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.ShareUtils;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.dialog.SleepTimerDialog;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+import rx.Observable;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.schedulers.Schedulers;
+
+
+/**
+ * Provides general features which are both needed for playing audio and video
+ * files.
+ */
+public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener {
+ private static final String TAG = "MediaplayerActivity";
+ private static final String PREFS = "MediaPlayerActivityPreferences";
+ private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
+
+ protected PlaybackController controller;
+
+ protected TextView txtvPosition;
+ protected TextView txtvLength;
+ protected SeekBar sbPosition;
+ protected ImageButton butRev;
+ protected TextView txtvRev;
+ protected ImageButton butPlay;
+ protected ImageButton butFF;
+ protected TextView txtvFF;
+ protected ImageButton butSkip;
+
+ protected boolean showTimeLeft = false;
+
+ private boolean isFavorite = false;
+
+ private PlaybackController newPlaybackController() {
+ return new PlaybackController(this, false) {
+
+ @Override
+ public void setupGUI() {
+ MediaplayerActivity.this.setupGUI();
+ }
+
+ @Override
+ public void onPositionObserverUpdate() {
+ MediaplayerActivity.this.onPositionObserverUpdate();
+ }
+
+ @Override
+ public void onBufferStart() {
+ MediaplayerActivity.this.onBufferStart();
+ }
+
+ @Override
+ public void onBufferEnd() {
+ MediaplayerActivity.this.onBufferEnd();
+ }
+
+ @Override
+ public void onBufferUpdate(float progress) {
+ MediaplayerActivity.this.onBufferUpdate(progress);
+ }
+
+ @Override
+ public void handleError(int code) {
+ MediaplayerActivity.this.handleError(code);
+ }
+
+ @Override
+ public void onReloadNotification(int code) {
+ MediaplayerActivity.this.onReloadNotification(code);
+ }
+
+ @Override
+ public void onSleepTimerUpdate() {
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public ImageButton getPlayButton() {
+ return butPlay;
+ }
+
+ @Override
+ public void postStatusMsg(int msg, boolean showToast) {
+ MediaplayerActivity.this.postStatusMsg(msg, showToast);
+ }
+
+ @Override
+ public void clearStatusMsg() {
+ MediaplayerActivity.this.clearStatusMsg();
+ }
+
+ @Override
+ public boolean loadMediaInfo() {
+ return MediaplayerActivity.this.loadMediaInfo();
+ }
+
+ @Override
+ public void onAwaitingVideoSurface() {
+ MediaplayerActivity.this.onAwaitingVideoSurface();
+ }
+
+ @Override
+ public void onServiceQueried() {
+ MediaplayerActivity.this.onServiceQueried();
+ }
+
+ @Override
+ public void onShutdownNotification() {
+ finish();
+ }
+
+ @Override
+ public void onPlaybackEnd() {
+ finish();
+ }
+
+ @Override
+ public void onPlaybackSpeedChange() {
+ MediaplayerActivity.this.onPlaybackSpeedChange();
+ }
+
+ @Override
+ protected void setScreenOn(boolean enable) {
+ super.setScreenOn(enable);
+ MediaplayerActivity.this.setScreenOn(enable);
+ }
+
+ @Override
+ public void onSetSpeedAbilityChanged() {
+ MediaplayerActivity.this.onSetSpeedAbilityChanged();
+ }
+ };
+ }
+
+ protected void onSetSpeedAbilityChanged() {
+ Log.d(TAG, "onSetSpeedAbilityChanged()");
+ updatePlaybackSpeedButton();
+ }
+
+ protected void onPlaybackSpeedChange() {
+ updatePlaybackSpeedButtonText();
+ }
+
+ protected void onServiceQueried() {
+ supportInvalidateOptionsMenu();
+ }
+
+ protected void chooseTheme() {
+ setTheme(UserPreferences.getTheme());
+ }
+
+ protected void setScreenOn(boolean enable) {
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ chooseTheme();
+ super.onCreate(savedInstanceState);
+
+ Log.d(TAG, "onCreate()");
+ StorageUtils.checkStorageAvailability(this);
+
+ orientation = getResources().getConfiguration().orientation;
+ getWindow().setFormat(PixelFormat.TRANSPARENT);
+ }
+
+ @Override
+ protected void onPause() {
+ if(controller != null) {
+ controller.reinitServiceIfPaused();
+ controller.pause();
+ }
+ super.onPause();
+ }
+
+ /**
+ * Should be used to switch to another player activity if the mime type is
+ * not the correct one for the current activity.
+ */
+ protected abstract void onReloadNotification(int notificationCode);
+
+ /**
+ * Should be used to inform the user that the PlaybackService is currently
+ * buffering.
+ */
+ protected abstract void onBufferStart();
+
+ /**
+ * Should be used to hide the view that was showing the 'buffering'-message.
+ */
+ protected abstract void onBufferEnd();
+
+ protected void onBufferUpdate(float progress) {
+ if (sbPosition != null) {
+ sbPosition.setSecondaryProgress((int) progress * sbPosition.getMax());
+ }
+ }
+
+ /**
+ * Current screen orientation.
+ */
+ protected int orientation;
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (controller != null) {
+ controller.release();
+ }
+ controller = newPlaybackController();
+ }
+
+ @Override
+ protected void onStop() {
+ Log.d(TAG, "onStop()");
+ if (controller != null) {
+ controller.release();
+ controller = null; // prevent leak
+ }
+ super.onStop();
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ Glide.get(this).trimMemory(level);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ Glide.get(this).clearMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+// requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.mediaplayer, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ if (controller == null) {
+ return false;
+ }
+ Playable media = controller.getMedia();
+
+ menu.findItem(R.id.support_item).setVisible(
+ media != null && media.getPaymentLink() != null &&
+ (media instanceof FeedMedia) &&
+ ((FeedMedia) media).getItem() != null &&
+ ((FeedMedia) media).getItem().getFlattrStatus().flattrable()
+ );
+
+ boolean hasWebsiteLink = media != null && media.getWebsiteLink() != null;
+ menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink);
+
+ boolean isItemAndHasLink = media != null && (media instanceof FeedMedia) &&
+ ((FeedMedia) media).getItem() != null && ((FeedMedia) media).getItem().getLink() != null;
+ menu.findItem(R.id.share_link_item).setVisible(isItemAndHasLink);
+ menu.findItem(R.id.share_link_with_position_item).setVisible(isItemAndHasLink);
+
+ boolean isItemHasDownloadLink = media != null && (media instanceof FeedMedia) && ((FeedMedia) media).getDownload_url() != null;
+ menu.findItem(R.id.share_download_url_item).setVisible(isItemHasDownloadLink);
+ menu.findItem(R.id.share_download_url_with_position_item).setVisible(isItemHasDownloadLink);
+
+ menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink);
+
+ menu.findItem(R.id.add_to_favorites_item).setVisible(false);
+ menu.findItem(R.id.remove_from_favorites_item).setVisible(false);
+ if(media != null && media instanceof FeedMedia) {
+ menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite);
+ menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite);
+ }
+
+ boolean sleepTimerSet = controller.sleepTimerActive();
+ boolean sleepTimerNotSet = controller.sleepTimerNotActive();
+ menu.findItem(R.id.set_sleeptimer_item).setVisible(sleepTimerNotSet);
+ menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerSet);
+
+ if (this instanceof AudioplayerActivity) {
+ int[] attrs = {R.attr.action_bar_icon_color};
+ TypedArray ta = obtainStyledAttributes(UserPreferences.getTheme(), attrs);
+ int textColor = ta.getColor(0, Color.GRAY);
+ ta.recycle();
+ menu.findItem(R.id.audio_controls).setIcon(new IconDrawable(this,
+ FontAwesomeIcons.fa_sliders).color(textColor).actionBarSize());
+ } else {
+ menu.findItem(R.id.audio_controls).setVisible(false);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (controller == null) {
+ return false;
+ }
+ Playable media = controller.getMedia();
+ if (item.getItemId() == android.R.id.home) {
+ Intent intent = new Intent(MediaplayerActivity.this,
+ MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ return true;
+ } else {
+ if (media != null) {
+ switch (item.getItemId()) {
+ case R.id.add_to_favorites_item:
+ if(media instanceof FeedMedia) {
+ FeedItem feedItem = ((FeedMedia)media).getItem();
+ if(feedItem != null) {
+ DBWriter.addFavoriteItem(feedItem);
+ isFavorite = true;
+ invalidateOptionsMenu();
+ Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+ break;
+ case R.id.remove_from_favorites_item:
+ if(media instanceof FeedMedia) {
+ FeedItem feedItem = ((FeedMedia)media).getItem();
+ if(feedItem != null) {
+ DBWriter.removeFavoriteItem(feedItem);
+ isFavorite = false;
+ invalidateOptionsMenu();
+ Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+ break;
+ case R.id.disable_sleeptimer_item:
+ if (controller.serviceAvailable()) {
+
+ MaterialDialog.Builder stDialog = new MaterialDialog.Builder(this);
+ stDialog.title(R.string.sleep_timer_label);
+ stDialog.content(getString(R.string.time_left_label)
+ + Converter.getDurationStringLong((int) controller
+ .getSleepTimerTimeLeft()));
+ stDialog.positiveText(R.string.disable_sleeptimer_label);
+ stDialog.negativeText(R.string.cancel_label);
+ stDialog.onPositive((dialog, which) -> {
+ dialog.dismiss();
+ controller.disableSleepTimer();
+ });
+ stDialog.onNegative((dialog, which) -> dialog.dismiss());
+ stDialog.build().show();
+ }
+ break;
+ case R.id.set_sleeptimer_item:
+ if (controller.serviceAvailable()) {
+ SleepTimerDialog td = new SleepTimerDialog(this) {
+ @Override
+ public void onTimerSet(long millis, boolean shakeToReset, boolean vibrate) {
+ controller.setSleepTimer(millis, shakeToReset, vibrate);
+ }
+ };
+ td.createNewDialog().show();
+ }
+ break;
+ case R.id.audio_controls:
+ MaterialDialog dialog = new MaterialDialog.Builder(this)
+ .title(R.string.audio_controls)
+ .customView(R.layout.audio_controls, true)
+ .neutralText(R.string.close_label)
+ .onNeutral((dialog1, which) -> {
+ final SeekBar left = (SeekBar) dialog1.findViewById(R.id.volume_left);
+ final SeekBar right = (SeekBar) dialog1.findViewById(R.id.volume_right);
+ UserPreferences.setVolume(left.getProgress(), right.getProgress());
+ })
+ .show();
+ final SeekBar barPlaybackSpeed = (SeekBar) dialog.findViewById(R.id.playback_speed);
+ final Button butDecSpeed = (Button) dialog.findViewById(R.id.butDecSpeed);
+ butDecSpeed.setOnClickListener(v -> {
+ if(controller != null && controller.canSetPlaybackSpeed()) {
+ barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 2);
+ } else {
+ VariableSpeedDialog.showGetPluginDialog(this);
+ }
+ });
+ final Button butIncSpeed = (Button) dialog.findViewById(R.id.butIncSpeed);
+ butIncSpeed.setOnClickListener(v -> {
+ if(controller != null && controller.canSetPlaybackSpeed()) {
+ barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 2);
+ } else {
+ VariableSpeedDialog.showGetPluginDialog(this);
+ }
+ });
+
+ final TextView txtvPlaybackSpeed = (TextView) dialog.findViewById(R.id.txtvPlaybackSpeed);
+ float currentSpeed = 1.0f;
+ try {
+ currentSpeed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
+ } catch (NumberFormatException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ UserPreferences.setPlaybackSpeed(String.valueOf(currentSpeed));
+ }
+
+ txtvPlaybackSpeed.setText(String.format("%.2fx", currentSpeed));
+ barPlaybackSpeed.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if(controller != null && controller.canSetPlaybackSpeed()) {
+ float playbackSpeed = (progress + 10) / 20.0f;
+ controller.setPlaybackSpeed(playbackSpeed);
+ String speedPref = String.format(Locale.US, "%.2f", playbackSpeed);
+ UserPreferences.setPlaybackSpeed(speedPref);
+ String speedStr = String.format("%.2fx", playbackSpeed);
+ txtvPlaybackSpeed.setText(speedStr);
+ } else if(fromUser) {
+ float speed = Float.valueOf(UserPreferences.getPlaybackSpeed());
+ barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress((int) (20 * speed) - 10));
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if(controller != null && !controller.canSetPlaybackSpeed()) {
+ VariableSpeedDialog.showGetPluginDialog(MediaplayerActivity.this);
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ barPlaybackSpeed.setProgress((int) (20 * currentSpeed) - 10);
+
+ final SeekBar barLeftVolume = (SeekBar) dialog.findViewById(R.id.volume_left);
+ barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage());
+ final SeekBar barRightVolume = (SeekBar) dialog.findViewById(R.id.volume_right);
+ barRightVolume.setProgress(UserPreferences.getRightVolumePercentage());
+ final CheckBox stereoToMono = (CheckBox) dialog.findViewById(R.id.stereo_to_mono);
+ stereoToMono.setChecked(UserPreferences.stereoToMono());
+ if (controller != null && !controller.canDownmix()) {
+ stereoToMono.setEnabled(false);
+ String sonicOnly = getString(R.string.sonic_only);
+ stereoToMono.setText(stereoToMono.getText() + " [" + sonicOnly + "]");
+ }
+
+ barLeftVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ controller.setVolume(
+ Converter.getVolumeFromPercentage(progress),
+ Converter.getVolumeFromPercentage(barRightVolume.getProgress()));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ barRightVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ controller.setVolume(
+ Converter.getVolumeFromPercentage(barLeftVolume.getProgress()),
+ Converter.getVolumeFromPercentage(progress));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ UserPreferences.stereoToMono(isChecked);
+ if (controller != null) {
+ controller.setDownmix(isChecked);
+ }
+ });
+ break;
+ case R.id.visit_website_item:
+ Uri uri = Uri.parse(media.getWebsiteLink());
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.support_item:
+ if (media instanceof FeedMedia) {
+ DBTasks.flattrItemIfLoggedIn(this, ((FeedMedia) media).getItem());
+ }
+ break;
+ case R.id.share_link_item:
+ if (media instanceof FeedMedia) {
+ ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem());
+ }
+ break;
+ case R.id.share_download_url_item:
+ if (media instanceof FeedMedia) {
+ ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem());
+ }
+ break;
+ case R.id.share_link_with_position_item:
+ if (media instanceof FeedMedia) {
+ ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem(), true);
+ }
+ break;
+ case R.id.share_download_url_with_position_item:
+ if (media instanceof FeedMedia) {
+ ShareUtils.shareFeedItemDownloadLink(this, ((FeedMedia) media).getItem(), true);
+ }
+ break;
+ default:
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume()");
+ StorageUtils.checkStorageAvailability(this);
+ if(controller != null) {
+ controller.init();
+ }
+ }
+
+ /**
+ * Called by 'handleStatus()' when the PlaybackService is waiting for
+ * a video surface.
+ */
+ protected abstract void onAwaitingVideoSurface();
+
+ protected abstract void postStatusMsg(int resId, boolean showToast);
+
+ protected abstract void clearStatusMsg();
+
+ protected void onPositionObserverUpdate() {
+ if (controller == null || txtvPosition == null || txtvLength == null) {
+ return;
+ }
+ int currentPosition = controller.getPosition();
+ int duration = controller.getDuration();
+ Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
+ if (currentPosition == PlaybackService.INVALID_TIME ||
+ duration == PlaybackService.INVALID_TIME) {
+ Log.w(TAG, "Could not react to position observer update because of invalid time");
+ return;
+ }
+ txtvPosition.setText(Converter.getDurationStringLong(currentPosition));
+ if (showTimeLeft) {
+ txtvLength.setText("-" + Converter.getDurationStringLong(duration - currentPosition));
+ } else {
+ txtvLength.setText(Converter.getDurationStringLong(duration));
+ }
+ updateProgressbarPosition(currentPosition, duration);
+ }
+
+ private void updateProgressbarPosition(int position, int duration) {
+ Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration + ")");
+ if(sbPosition == null) {
+ return;
+ }
+ float progress = ((float) position) / duration;
+ sbPosition.setProgress((int) (progress * sbPosition.getMax()));
+ }
+
+ /**
+ * Load information about the media that is going to be played or currently
+ * being played. This method will be called when the activity is connected
+ * to the PlaybackService to ensure that the activity has the right
+ * FeedMedia object.
+ */
+ protected boolean loadMediaInfo() {
+ Log.d(TAG, "loadMediaInfo()");
+ Playable media = controller.getMedia();
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
+ if (media != null) {
+ onPositionObserverUpdate();
+ checkFavorite();
+ updatePlaybackSpeedButton();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void updatePlaybackSpeedButton() {
+ // Only meaningful on AudioplayerActivity, where it is overridden.
+ }
+
+ protected void updatePlaybackSpeedButtonText() {
+ // Only meaningful on AudioplayerActivity, where it is overridden.
+ }
+
+
+ protected void setupGUI() {
+ setContentView(getContentViewResourceId());
+ sbPosition = (SeekBar) findViewById(R.id.sbPosition);
+ txtvPosition = (TextView) findViewById(R.id.txtvPosition);
+
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
+ Log.d("timeleft", showTimeLeft ? "true" : "false");
+ txtvLength = (TextView) findViewById(R.id.txtvLength);
+ if (txtvLength != null) {
+ txtvLength.setOnClickListener(v -> {
+ showTimeLeft = !showTimeLeft;
+ Playable media = controller.getMedia();
+ if (media == null) {
+ return;
+ }
+
+ String length;
+ if (showTimeLeft) {
+ length = "-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition());
+ } else {
+ length = Converter.getDurationStringLong(media.getDuration());
+ }
+ txtvLength.setText(length);
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft);
+ editor.apply();
+ Log.d("timeleft on click", showTimeLeft ? "true" : "false");
+ });
+ }
+
+ butRev = (ImageButton) findViewById(R.id.butRev);
+ txtvRev = (TextView) findViewById(R.id.txtvRev);
+ if (txtvRev != null) {
+ txtvRev.setText(String.valueOf(UserPreferences.getRewindSecs()));
+ }
+ butPlay = (ImageButton) findViewById(R.id.butPlay);
+ butFF = (ImageButton) findViewById(R.id.butFF);
+ txtvFF = (TextView) findViewById(R.id.txtvFF);
+ if (txtvFF != null) {
+ txtvFF.setText(String.valueOf(UserPreferences.getFastFowardSecs()));
+ }
+ butSkip = (ImageButton) findViewById(R.id.butSkip);
+
+ // SEEKBAR SETUP
+
+ sbPosition.setOnSeekBarChangeListener(this);
+
+ // BUTTON SETUP
+
+ if (butRev != null) {
+ butRev.setOnClickListener(v -> onRewind());
+ butRev.setOnLongClickListener(new View.OnLongClickListener() {
+
+ int choice;
+
+ @Override
+ public boolean onLongClick(View v) {
+ int checked = 0;
+ int rewindSecs = UserPreferences.getRewindSecs();
+ final int[] values = getResources().getIntArray(R.array.seek_delta_values);
+ final String[] choices = new String[values.length];
+ for (int i = 0; i < values.length; i++) {
+ if (rewindSecs == values[i]) {
+ checked = i;
+ }
+ choices[i] = String.valueOf(values[i]) + " " + getString(R.string.time_seconds);
+ }
+ choice = values[checked];
+ AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this);
+ builder.setTitle(R.string.pref_rewind);
+ builder.setSingleChoiceItems(choices, checked,
+ (dialog, which) -> {
+ choice = values[which];
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setPrefRewindSecs(choice);
+ if(txtvRev != null){
+ txtvRev.setText(String.valueOf(choice));
+ }
+ });
+ builder.create().show();
+ return true;
+ }
+ });
+ }
+
+ butPlay.setOnClickListener(v -> onPlayPause());
+
+ if (butFF != null) {
+ butFF.setOnClickListener(v -> onFastForward());
+ butFF.setOnLongClickListener(new View.OnLongClickListener() {
+
+ int choice;
+
+ @Override
+ public boolean onLongClick(View v) {
+ int checked = 0;
+ int rewindSecs = UserPreferences.getFastFowardSecs();
+ final int[] values = getResources().getIntArray(R.array.seek_delta_values);
+ final String[] choices = new String[values.length];
+ for (int i = 0; i < values.length; i++) {
+ if (rewindSecs == values[i]) {
+ checked = i;
+ }
+ choices[i] = String.valueOf(values[i]) + " " + getString(R.string.time_seconds);
+ }
+ choice = values[checked];
+ AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this);
+ builder.setTitle(R.string.pref_fast_forward);
+ builder.setSingleChoiceItems(choices, checked,
+ (dialog, which) -> {
+ choice = values[which];
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setPrefFastForwardSecs(choice);
+ if(txtvFF != null) {
+ txtvFF.setText(String.valueOf(choice));
+ }
+ });
+ builder.create().show();
+ return true;
+ }
+ });
+ }
+
+ if (butSkip != null) {
+ butSkip.setOnClickListener(v -> sendBroadcast(new Intent(PlaybackService.ACTION_SKIP_CURRENT_EPISODE)));
+ }
+ }
+
+ protected void onRewind() {
+ if (controller == null) {
+ return;
+ }
+ int curr = controller.getPosition();
+ controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000);
+ }
+
+ protected void onPlayPause() {
+ if(controller == null) {
+ return;
+ }
+ controller.playPause();
+ }
+
+ protected void onFastForward() {
+ if (controller == null) {
+ return;
+ }
+ int curr = controller.getPosition();
+ controller.seekTo(curr + UserPreferences.getFastFowardSecs() * 1000);
+ }
+
+ protected abstract int getContentViewResourceId();
+
+ void handleError(int errorCode) {
+ final AlertDialog.Builder errorDialog = new AlertDialog.Builder(this);
+ errorDialog.setTitle(R.string.error_label);
+ errorDialog.setMessage(MediaPlayerError.getErrorString(this, errorCode));
+ errorDialog.setNeutralButton("OK",
+ (dialog, which) -> {
+ dialog.dismiss();
+ finish();
+ }
+ );
+ errorDialog.create().show();
+ }
+
+ float prog;
+
+ @Override
+ public void onProgressChanged (SeekBar seekBar,int progress, boolean fromUser) {
+ if (controller == null || txtvLength == null) {
+ return;
+ }
+ prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, txtvPosition);
+ if (showTimeLeft && prog != 0) {
+ int duration = controller.getDuration();
+ String length = "-" + Converter.getDurationStringLong(duration - (int) (prog * duration));
+ txtvLength.setText(length);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (controller != null) {
+ controller.onSeekBarStartTrackingTouch(seekBar);
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (controller != null) {
+ controller.onSeekBarStopTrackingTouch(seekBar, prog);
+ }
+ }
+
+ private void checkFavorite() {
+ Playable playable = controller.getMedia();
+ if (playable != null && playable instanceof FeedMedia) {
+ FeedItem feedItem = ((FeedMedia) playable).getItem();
+ if (feedItem != null) {
+ Observable.fromCallable(() -> DBReader.getFeedItem(feedItem.getId()))
+ .subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ item -> {
+ boolean isFav = item.isTagged(FeedItem.TAG_FAVORITE);
+ if (isFavorite != isFav) {
+ isFavorite = isFav;
+ invalidateOptionsMenu();
+ }
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ }
+ );
+ }
+ }
+ }
+
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/free/java/de/danoeh/antennapod/fragment/ItemFragment.java
new file mode 100644
index 000000000..0e11a5a17
--- /dev/null
+++ b/app/src/free/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -0,0 +1,592 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.GestureDetectorCompat;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.bumptech.glide.Glide;
+import com.joanzapata.iconify.Iconify;
+import com.joanzapata.iconify.widget.IconButton;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.adapter.DefaultActionButtonCallback;
+import de.danoeh.antennapod.core.event.DownloadEvent;
+import de.danoeh.antennapod.core.event.DownloaderUpdate;
+import de.danoeh.antennapod.core.event.FeedItemEvent;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.glide.ApGlideSettings;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.Downloader;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.DateUtils;
+import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.core.util.LongList;
+import de.danoeh.antennapod.core.util.ShareUtils;
+import de.danoeh.antennapod.core.util.playback.Timeline;
+import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
+import de.danoeh.antennapod.view.OnSwipeGesture;
+import de.danoeh.antennapod.view.SwipeGestureDetector;
+import de.greenrobot.event.EventBus;
+import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.schedulers.Schedulers;
+
+/**
+ * Displays information about a FeedItem and actions.
+ */
+public class ItemFragment extends Fragment implements OnSwipeGesture {
+
+ private static final String TAG = "ItemFragment";
+
+ private static final int EVENTS = EventDistributor.UNREAD_ITEMS_UPDATE;
+
+ private static final String ARG_FEEDITEMS = "feeditems";
+ private static final String ARG_FEEDITEM_POS = "feeditem_pos";
+
+ private GestureDetectorCompat headerGestureDetector;
+ private GestureDetectorCompat webviewGestureDetector;
+
+ /**
+ * Creates a new instance of an ItemFragment
+ *
+ * @param feeditem The ID of the FeedItem that should be displayed.
+ * @return The ItemFragment instance
+ */
+ public static ItemFragment newInstance(long feeditem) {
+ return newInstance(new long[] { feeditem }, 0);
+ }
+
+ /**
+ * Creates a new instance of an ItemFragment
+ *
+ * @param feeditems The IDs of the FeedItems that belong to the same list
+ * @param feedItemPos The position of the FeedItem that is currently shown
+ * @return The ItemFragment instance
+ */
+ public static ItemFragment newInstance(long[] feeditems, int feedItemPos) {
+ ItemFragment fragment = new ItemFragment();
+ Bundle args = new Bundle();
+ args.putLongArray(ARG_FEEDITEMS, feeditems);
+ args.putInt(ARG_FEEDITEM_POS, feedItemPos);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ private boolean itemsLoaded = false;
+ private long[] feedItems;
+ private int feedItemPos;
+ private FeedItem item;
+ private String webviewData;
+ private List<Downloader> downloaderList;
+
+ private ViewGroup root;
+ private WebView webvDescription;
+ private TextView txtvPodcast;
+ private TextView txtvTitle;
+ private TextView txtvDuration;
+ private TextView txtvPublished;
+ private ImageView imgvCover;
+ private ProgressBar progbarDownload;
+ private ProgressBar progbarLoading;
+ private IconButton butAction1;
+ private IconButton butAction2;
+ private Menu popupMenu;
+
+ private Subscription subscription;
+
+ /**
+ * URL that was selected via long-press.
+ */
+ private String selectedURL;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ setHasOptionsMenu(true);
+
+ feedItems = getArguments().getLongArray(ARG_FEEDITEMS);
+ feedItemPos = getArguments().getInt(ARG_FEEDITEM_POS);
+
+ headerGestureDetector = new GestureDetectorCompat(getActivity(), new SwipeGestureDetector(this));
+ webviewGestureDetector = new GestureDetectorCompat(getActivity(), new SwipeGestureDetector(this) {
+ // necessary for the longclick context menu to work properly
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View layout = inflater.inflate(R.layout.feeditem_fragment, container, false);
+
+ root = (ViewGroup) layout.findViewById(R.id.content_root);
+
+ LinearLayout header = (LinearLayout) root.findViewById(R.id.header);
+ if(feedItems.length > 0) {
+ header.setOnTouchListener((v, event) -> headerGestureDetector.onTouchEvent(event));
+ }
+
+ txtvPodcast = (TextView) layout.findViewById(R.id.txtvPodcast);
+ txtvPodcast.setOnClickListener(v -> openPodcast());
+ txtvTitle = (TextView) layout.findViewById(R.id.txtvTitle);
+ if(Build.VERSION.SDK_INT >= 23) {
+ txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
+ }
+ txtvDuration = (TextView) layout.findViewById(R.id.txtvDuration);
+ txtvPublished = (TextView) layout.findViewById(R.id.txtvPublished);
+ if (Build.VERSION.SDK_INT >= 14) { // ellipsize is causing problems on old versions, see #448
+ txtvTitle.setEllipsize(TextUtils.TruncateAt.END);
+ }
+ webvDescription = (WebView) layout.findViewById(R.id.webvDescription);
+ if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) {
+ if (Build.VERSION.SDK_INT >= 11
+ && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ webvDescription.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.black));
+ }
+ webvDescription.getSettings().setUseWideViewPort(false);
+ webvDescription.getSettings().setLayoutAlgorithm(
+ WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+ webvDescription.getSettings().setLoadWithOverviewMode(true);
+ if(feedItems.length > 0) {
+ webvDescription.setOnLongClickListener(webViewLongClickListener);
+ }
+ webvDescription.setOnTouchListener((v, event) -> webviewGestureDetector.onTouchEvent(event));
+ webvDescription.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ if(IntentUtils.isCallable(getActivity(), intent)) {
+ startActivity(intent);
+ }
+ return true;
+ }
+ });
+ registerForContextMenu(webvDescription);
+
+ imgvCover = (ImageView) layout.findViewById(R.id.imgvCover);
+ imgvCover.setOnClickListener(v -> openPodcast());
+ progbarDownload = (ProgressBar) layout.findViewById(R.id.progbarDownload);
+ progbarLoading = (ProgressBar) layout.findViewById(R.id.progbarLoading);
+ butAction1 = (IconButton) layout.findViewById(R.id.butAction1);
+ butAction2 = (IconButton) layout.findViewById(R.id.butAction2);
+
+ butAction1.setOnClickListener(v -> {
+ if (item == null) {
+ return;
+ }
+ DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getActivity());
+ actionButtonCallback.onActionButtonPressed(item, item.isTagged(FeedItem.TAG_QUEUE) ?
+ LongList.of(item.getId()) : new LongList(0));
+ FeedMedia media = item.getMedia();
+ if (media != null && media.isDownloaded()) {
+ // playback was started, dialog should close itself
+ ((MainActivity) getActivity()).dismissChildFragment();
+ }
+ });
+
+ butAction2.setOnClickListener(v -> {
+ if (item == null) {
+ return;
+ }
+
+ if (item.hasMedia()) {
+ FeedMedia media = item.getMedia();
+ if (!media.isDownloaded()) {
+ DBTasks.playMedia(getActivity(), media, true, true, true);
+ ((MainActivity) getActivity()).dismissChildFragment();
+ } else {
+ DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId());
+ }
+ } else if (item.getLink() != null) {
+ Uri uri = Uri.parse(item.getLink());
+ getActivity().startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+ });
+
+ return layout;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ load();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ EventDistributor.getInstance().register(contentUpdate);
+ EventBus.getDefault().registerSticky(this);
+ if(itemsLoaded) {
+ progbarLoading.setVisibility(View.GONE);
+ updateAppearance();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ EventDistributor.getInstance().unregister(contentUpdate);
+ EventBus.getDefault().unregister(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if(subscription != null) {
+ subscription.unsubscribe();
+ }
+ if (webvDescription != null && root != null) {
+ root.removeView(webvDescription);
+ webvDescription.destroy();
+ }
+ }
+
+ @Override
+ public boolean onSwipeLeftToRight() {
+ Log.d(TAG, "onSwipeLeftToRight()");
+ feedItemPos = feedItemPos - 1;
+ if(feedItemPos < 0) {
+ feedItemPos = feedItems.length - 1;
+ }
+ load();
+ return true;
+ }
+
+ @Override
+ public boolean onSwipeRightToLeft() {
+ Log.d(TAG, "onSwipeRightToLeft()");
+ feedItemPos = (feedItemPos + 1) % feedItems.length;
+ load();
+ return true;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if(!isAdded() || item == null) {
+ return;
+ }
+// ((CastEnabledActivity) getActivity()).requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ inflater.inflate(R.menu.feeditem_options, menu);
+ popupMenu = menu;
+ if (item.hasMedia()) {
+ FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null);
+ } else {
+ // these are already available via button1 and button2
+ FeedItemMenuHandler.onPrepareMenu(popupMenuInterface, item, true, null,
+ R.id.mark_read_item, R.id.visit_website_item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ switch(menuItem.getItemId()) {
+ case R.id.open_podcast:
+ openPodcast();
+ return true;
+ default:
+ try {
+ return FeedItemMenuHandler.onMenuItemClicked(getActivity(), menuItem.getItemId(), item);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
+ return true;
+ }
+ }
+ }
+
+ private final FeedItemMenuHandler.MenuInterface popupMenuInterface = new FeedItemMenuHandler.MenuInterface() {
+ @Override
+ public void setItemVisibility(int id, boolean visible) {
+ MenuItem item = popupMenu.findItem(id);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
+ };
+
+
+ private void onFragmentLoaded() {
+ if (webviewData != null) {
+ webvDescription.loadDataWithBaseURL(null, webviewData, "text/html", "utf-8", "about:blank");
+ }
+ updateAppearance();
+ }
+
+ private void updateAppearance() {
+ if (item == null) {
+ Log.d(TAG, "updateAppearance item is null");
+ return;
+ }
+ getActivity().supportInvalidateOptionsMenu();
+ txtvPodcast.setText(item.getFeed().getTitle());
+ txtvTitle.setText(item.getTitle());
+
+ if (item.getPubDate() != null) {
+ String pubDateStr = DateUtils.formatAbbrev(getActivity(), item.getPubDate());
+ txtvPublished.setText(pubDateStr);
+ }
+
+ Glide.with(getActivity())
+ .load(item.getImageLocation())
+ .placeholder(R.color.light_gray)
+ .error(R.color.light_gray)
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .fitCenter()
+ .dontAnimate()
+ .into(imgvCover);
+
+ progbarDownload.setVisibility(View.GONE);
+ if (item.hasMedia() && downloaderList != null) {
+ for (Downloader downloader : downloaderList) {
+ if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
+ && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) {
+ progbarDownload.setVisibility(View.VISIBLE);
+ progbarDownload.setProgress(downloader.getDownloadRequest().getProgressPercent());
+ }
+ }
+ }
+
+ FeedMedia media = item.getMedia();
+ String butAction1Icon = null;
+ int butAction1Text = 0;
+ String butAction2Icon = null;
+ int butAction2Text = 0;
+ if (media == null) {
+ if (!item.isPlayed()) {
+ butAction1Icon = "{fa-check 24sp}";
+ butAction1Text = R.string.mark_read_label;
+ }
+ if (item.getLink() != null) {
+ butAction2Icon = "{md-web 24sp}";
+ butAction2Text = R.string.visit_website_label;
+ }
+ } else {
+ if(media.getDuration() > 0) {
+ txtvDuration.setText(Converter.getDurationStringLong(media.getDuration()));
+ }
+ boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media);
+ if (!media.isDownloaded()) {
+ butAction2Icon = "{md-settings-input-antenna 24sp}";
+ butAction2Text = R.string.stream_label;
+ } else {
+ butAction2Icon = "{md-delete 24sp}";
+ butAction2Text = R.string.remove_label;
+ }
+ if (isDownloading) {
+ butAction1Icon = "{md-cancel 24sp}";
+ butAction1Text = R.string.cancel_label;
+ } else if (media.isDownloaded()) {
+ butAction1Icon = "{md-play-arrow 24sp}";
+ butAction1Text = R.string.play_label;
+ } else {
+ butAction1Icon = "{md-file-download 24sp}";
+ butAction1Text = R.string.download_label;
+ }
+ }
+ if(butAction1Icon != null && butAction1Text != 0) {
+ butAction1.setText(butAction1Icon +"\u0020\u0020" + getActivity().getString(butAction1Text));
+ Iconify.addIcons(butAction1);
+ butAction1.setVisibility(View.VISIBLE);
+ } else {
+ butAction1.setVisibility(View.INVISIBLE);
+ }
+ if(butAction2Icon != null && butAction2Text != 0) {
+ butAction2.setText(butAction2Icon +"\u0020\u0020" + getActivity().getString(butAction2Text));
+ Iconify.addIcons(butAction2);
+ butAction2.setVisibility(View.VISIBLE);
+ } else {
+ butAction2.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ WebView.HitTestResult r = webvDescription.getHitTestResult();
+ if (r != null
+ && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
+ Log.d(TAG, "Link of webview was long-pressed. Extra: " + r.getExtra());
+ selectedURL = r.getExtra();
+ webvDescription.showContextMenu();
+ return true;
+ }
+ selectedURL = null;
+ return false;
+ }
+ };
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ boolean handled = selectedURL != null;
+ if (selectedURL != null) {
+ switch (item.getItemId()) {
+ case R.id.open_in_browser_item:
+ Uri uri = Uri.parse(selectedURL);
+ final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ if(IntentUtils.isCallable(getActivity(), intent)) {
+ getActivity().startActivity(intent);
+ }
+ break;
+ case R.id.share_url_item:
+ ShareUtils.shareLink(getActivity(), selectedURL);
+ break;
+ case R.id.copy_url_item:
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ ClipData clipData = ClipData.newPlainText(selectedURL,
+ selectedURL);
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(clipData);
+ } else {
+ android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setText(selectedURL);
+ }
+ Toast t = Toast.makeText(getActivity(),
+ R.string.copied_url_msg, Toast.LENGTH_SHORT);
+ t.show();
+ break;
+ default:
+ handled = false;
+ break;
+
+ }
+ selectedURL = null;
+ }
+ return handled;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ if (selectedURL != null) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ Uri uri = Uri.parse(selectedURL);
+ final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ if(IntentUtils.isCallable(getActivity(), intent)) {
+ menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE,
+ R.string.open_in_browser_label);
+ }
+ menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE,
+ R.string.copy_url_label);
+ menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE,
+ R.string.share_url_label);
+ menu.setHeaderTitle(selectedURL);
+ }
+ }
+
+ private void openPodcast() {
+ Fragment fragment = ItemlistFragment.newInstance(item.getFeedId());
+ ((MainActivity)getActivity()).loadChildFragment(fragment);
+ }
+
+ public void onEventMainThread(FeedItemEvent event) {
+ Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
+ for(FeedItem item : event.items) {
+ if(feedItems[feedItemPos] == item.getId()) {
+ load();
+ return;
+ }
+ }
+ }
+
+ public void onEventMainThread(DownloadEvent event) {
+ Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
+ DownloaderUpdate update = event.update;
+ downloaderList = update.downloaders;
+ if(item == null || item.getMedia() == null) {
+ return;
+ }
+ long mediaId = item.getMedia().getId();
+ if(ArrayUtils.contains(update.mediaIds, mediaId)) {
+ if (itemsLoaded && getActivity() != null) {
+ updateAppearance();
+ }
+ }
+ }
+
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((arg & EVENTS) != 0) {
+ load();
+ }
+ }
+ };
+
+ private void load() {
+ if(subscription != null) {
+ subscription.unsubscribe();
+ }
+ progbarLoading.setVisibility(View.VISIBLE);
+ subscription = Observable.fromCallable(this::loadInBackground)
+ .subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ progbarLoading.setVisibility(View.GONE);
+ item = result;
+ itemsLoaded = true;
+ onFragmentLoaded();
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ });
+ }
+
+ private FeedItem loadInBackground() {
+ FeedItem feedItem = DBReader.getFeedItem(feedItems[feedItemPos]);
+ if (feedItem != null) {
+ Timeline t = new Timeline(getActivity(), feedItem);
+ webviewData = t.processShownotes(false);
+ }
+ return feedItem;
+ }
+
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceController.java
new file mode 100644
index 000000000..5cd44c0a7
--- /dev/null
+++ b/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceController.java
@@ -0,0 +1,951 @@
+package de.danoeh.antennapod.preferences;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.TimePickerDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextWatcher;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.Toast;
+import android.widget.ListView;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.CrashReportWriter;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.AboutActivity;
+import de.danoeh.antennapod.activity.DirectoryChooserActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+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.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.GpodnetSyncService;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog;
+import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
+import de.danoeh.antennapod.dialog.ProxyDialog;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
+
+/**
+ * Sets up a preference UI that lets the user change user preferences.
+ */
+
+public class PreferenceController implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ 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";
+ 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_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[] 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));
+ }
+ }
+ }
+
+ /**
+ * Returns the preference activity that should be used on this device.
+ *
+ * @return PreferenceActivity if the API level is greater than 10, PreferenceActivityGingerbread otherwise.
+ */
+ public static Class<? extends Activity> getPreferenceActivity() {
+ if (Build.VERSION.SDK_INT > 10) {
+ return PreferenceActivity.class;
+ } else {
+ return PreferenceActivityGingerbread.class;
+ }
+ }
+
+ public void onCreate() {
+ final Activity activity = ui.getActivity();
+
+ if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ // disable expanded notification option on unsupported android versions
+ ui.findPreference(PreferenceController.PREF_EXPANDED_NOTIFICATION).setEnabled(false);
+ ui.findPreference(PreferenceController.PREF_EXPANDED_NOTIFICATION).setOnPreferenceClickListener(
+ preference -> {
+ Toast toast = Toast.makeText(activity,
+ R.string.pref_expand_notify_unsupport_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ return true;
+ }
+ );
+ }
+ ui.findPreference(PreferenceController.PREF_FLATTR_REVOKE).setOnPreferenceClickListener(
+ preference -> {
+ FlattrUtils.revokeAccessToken(activity);
+ checkItemVisibility();
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.PREF_ABOUT).setOnPreferenceClickListener(
+ preference -> {
+ activity.startActivity(new Intent(activity, AboutActivity.class));
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.STATISTICS).setOnPreferenceClickListener(
+ preference -> {
+ activity.startActivity(new Intent(activity, StatisticsActivity.class));
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.PREF_OPML_EXPORT).setOnPreferenceClickListener(
+ preference -> {
+ new OpmlExportWorker(activity).executeAsync();
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener(
+ preference -> {
+ if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT &&
+ Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ showChooseDataFolderDialog();
+ } else {
+ int readPermission = ActivityCompat.checkSelfPermission(
+ activity, Manifest.permission.READ_EXTERNAL_STORAGE);
+ int writePermission = ActivityCompat.checkSelfPermission(
+ activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ if (readPermission == PackageManager.PERMISSION_GRANTED &&
+ writePermission == PackageManager.PERMISSION_GRANTED) {
+ openDirectoryChooser();
+ } else {
+ requestPermission();
+ }
+ }
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR)
+ .setOnPreferenceClickListener(
+ preference -> {
+ if (Build.VERSION.SDK_INT >= 19) {
+ showChooseDataFolderDialog();
+ } else {
+ Intent intent = new Intent(activity, DirectoryChooserActivity.class);
+ activity.startActivityForResult(intent,
+ DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED);
+ }
+ return true;
+ }
+ );
+ ui.findPreference(UserPreferences.PREF_THEME)
+ .setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ Intent i = new Intent(activity, MainActivity.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.finish();
+ activity.startActivity(i);
+ return true;
+ }
+ );
+ ui.findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)
+ .setOnPreferenceClickListener(preference -> {
+ showDrawerPreferencesDialog();
+ return true;
+ });
+
+ ui.findPreference(UserPreferences.PREF_COMPACT_NOTIFICATION_BUTTONS)
+ .setOnPreferenceClickListener(preference -> {
+ showNotificationButtonsDialog();
+ return true;
+ });
+
+ ui.findPreference(UserPreferences.PREF_UPDATE_INTERVAL)
+ .setOnPreferenceClickListener(preference -> {
+ showUpdateIntervalTimePreferencesDialog();
+ return true;
+ });
+
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL).setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ if (newValue instanceof Boolean) {
+ boolean enabled = (Boolean) newValue;
+ ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(enabled);
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(enabled);
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(enabled);
+ setSelectedNetworksEnabled(enabled && UserPreferences.isEnableAutodownloadWifiFilter());
+ }
+ return true;
+ });
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)
+ .setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ if (newValue instanceof Boolean) {
+ setSelectedNetworksEnabled((Boolean) newValue);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ );
+ ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS)
+ .setOnPreferenceChangeListener(
+ (preference, o) -> {
+ if (o instanceof String) {
+ try {
+ int value = Integer.parseInt((String) o);
+ if (1 <= value && value <= 50) {
+ setParallelDownloadsText(value);
+ return true;
+ }
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+ );
+ // validate and set correct value: number of downloads between 1 and 50 (inclusive)
+ final EditText ev = ((EditTextPreference) ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS)).getEditText();
+ ev.addTextChangedListener(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) {
+ if (s.length() > 0) {
+ try {
+ int value = Integer.parseInt(s.toString());
+ if (value <= 0) {
+ ev.setText("1");
+ } else if (value > 50) {
+ ev.setText("50");
+ }
+ } catch (NumberFormatException e) {
+ ev.setText("6");
+ }
+ ev.setSelection(ev.getText().length());
+ }
+ }
+ });
+ ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE)
+ .setOnPreferenceChangeListener(
+ (preference, o) -> {
+ if (o instanceof String) {
+ setEpisodeCacheSizeText(UserPreferences.readEpisodeCacheSize((String) o));
+ }
+ return true;
+ }
+ );
+ ui.findPreference(PreferenceController.PREF_PLAYBACK_SPEED_LAUNCHER)
+ .setOnPreferenceClickListener(preference -> {
+ VariableSpeedDialog.showDialog(activity);
+ return true;
+ });
+ ui.findPreference(PreferenceController.PREF_GPODNET_SETLOGIN_INFORMATION)
+ .setOnPreferenceClickListener(preference -> {
+ AuthenticationDialog dialog = new AuthenticationDialog(activity,
+ R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(),
+ null) {
+
+ @Override
+ protected void onConfirmed(String username, String password, boolean saveUsernamePassword) {
+ GpodnetPreferences.setPassword(password);
+ }
+ };
+ dialog.show();
+ return true;
+ });
+ ui.findPreference(PreferenceController.PREF_GPODNET_SYNC).
+ setOnPreferenceClickListener(preference -> {
+ 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();
+ Toast toast = Toast.makeText(activity, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT);
+ toast.show();
+ updateGpodnetPreferenceScreen();
+ return true;
+ });
+ ui.findPreference(PreferenceController.PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(
+ preference -> {
+ GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener(dialog -> updateGpodnetPreferenceScreen());
+ return true;
+ });
+
+ ui.findPreference(PreferenceController.PREF_AUTO_FLATTR_PREFS)
+ .setOnPreferenceClickListener(preference -> {
+ AutoFlattrPreferenceDialog.newAutoFlattrPreferenceDialog(activity,
+ new AutoFlattrPreferenceDialog.AutoFlattrPreferenceDialogInterface() {
+ @Override
+ public void onCancelled() {
+
+ }
+
+ @Override
+ public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue) {
+ UserPreferences.setAutoFlattrSettings(autoFlattrEnabled, autoFlattrValue);
+ checkItemVisibility();
+ }
+ });
+ return true;
+ });
+ ui.findPreference(UserPreferences.PREF_IMAGE_CACHE_SIZE).setOnPreferenceChangeListener(
+ (preference, o) -> {
+ if (o instanceof String) {
+ int newValue = Integer.parseInt((String) o) * 1024 * 1024;
+ if (newValue != UserPreferences.getImageCacheSize()) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(ui.getActivity());
+ dialog.setTitle(android.R.string.dialog_alert_title);
+ dialog.setMessage(R.string.pref_restart_required);
+ dialog.setPositiveButton(android.R.string.ok, null);
+ dialog.show();
+ }
+ return true;
+ }
+ return false;
+ }
+ );
+ ui.findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> {
+ ProxyDialog dialog = new ProxyDialog(ui.getActivity());
+ dialog.createDialog().show();
+ return true;
+ });
+ ui.findPreference(PREF_KNOWN_ISSUES).setOnPreferenceClickListener(preference -> {
+ openInBrowser("https://github.com/AntennaPod/AntennaPod/labels/bug");
+ return true;
+ });
+ ui.findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> {
+ openInBrowser("http://antennapod.org/faq.html");
+ return true;
+ });
+ ui.findPreference(PREF_SEND_CRASH_REPORT).setOnPreferenceClickListener(preference -> {
+ Intent emailIntent = new Intent(Intent.ACTION_SEND);
+ emailIntent.setType("text/plain");
+ emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"Martin.Fietz@gmail.com"});
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, "AntennaPod Crash Report");
+ emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe what you were doing when the app crashed");
+ // the attachment
+ emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(CrashReportWriter.getFile()));
+ String intentTitle = ui.getActivity().getString(R.string.send_email);
+ ui.getActivity().startActivity(Intent.createChooser(emailIntent, intentTitle));
+ return true;
+ });
+ //checks whether Google Play Services is installed on the device (condition necessary for Cast support)
+// ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setOnPreferenceChangeListener((preference, o) -> {
+// if (o instanceof Boolean && ((Boolean) o)) {
+// final int googlePlayServicesCheck = GoogleApiAvailability.getInstance()
+// .isGooglePlayServicesAvailable(ui.getActivity());
+// if (googlePlayServicesCheck == ConnectionResult.SUCCESS) {
+// return true;
+// } else {
+// GoogleApiAvailability.getInstance()
+// .getErrorDialog(ui.getActivity(), googlePlayServicesCheck, 0)
+// .show();
+// return false;
+// }
+// }
+// return true;
+// });
+ buildEpisodeCleanupPreference();
+ buildSmartMarkAsPlayedPreference();
+ buildAutodownloadSelectedNetworsPreference();
+ setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter());
+ }
+
+ private void openInBrowser(String url) {
+ try {
+ Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ ui.getActivity().startActivity(myIntent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(ui.getActivity(), R.string.pref_no_browser_found, Toast.LENGTH_LONG).show();
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ }
+
+ public void onResume() {
+ checkItemVisibility();
+ setUpdateIntervalText();
+ setParallelDownloadsText(UserPreferences.getParallelDownloads());
+ setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize());
+ setDataFolderText();
+ updateGpodnetPreferenceScreen();
+ }
+
+ @SuppressLint("NewApi")
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK &&
+ requestCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
+ String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR);
+
+ File path;
+ if(dir != null) {
+ path = new File(dir);
+ } else {
+ path = ui.getActivity().getExternalFilesDir(null);
+ }
+ String message = null;
+ final Context context= ui.getActivity().getApplicationContext();
+ if(!path.exists()) {
+ message = String.format(context.getString(R.string.folder_does_not_exist_error), dir);
+ } else if(!path.canRead()) {
+ message = String.format(context.getString(R.string.folder_not_readable_error), dir);
+ } else if(!path.canWrite()) {
+ message = String.format(context.getString(R.string.folder_not_writable_error), dir);
+ }
+
+ if(message == null) {
+ Log.d(TAG, "Setting data folder: " + dir);
+ UserPreferences.setDataFolder(dir);
+ setDataFolderText();
+ } else {
+ AlertDialog.Builder ab = new AlertDialog.Builder(ui.getActivity());
+ ab.setMessage(message);
+ ab.setPositiveButton(android.R.string.ok, null);
+ ab.show();
+ }
+ }
+ }
+
+
+ private void updateGpodnetPreferenceScreen() {
+ final boolean loggedIn = GpodnetPreferences.loggedIn();
+ 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_LOGOUT).setEnabled(loggedIn);
+ if(loggedIn) {
+ String format = ui.getActivity().getString(R.string.pref_gpodnet_login_status);
+ String summary = String.format(format, GpodnetPreferences.getUsername(),
+ GpodnetPreferences.getDeviceID());
+ ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setSummary(Html.fromHtml(summary));
+ } else {
+ ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setSummary(null);
+ }
+ ui.findPreference(PreferenceController.PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname());
+ }
+
+ private String[] getUpdateIntervalEntries(final String[] values) {
+ final Resources res = ui.getActivity().getResources();
+ String[] entries = new String[values.length];
+ for (int x = 0; x < values.length; x++) {
+ Integer v = Integer.parseInt(values[x]);
+ switch (v) {
+ case 0:
+ entries[x] = res.getString(R.string.pref_update_interval_hours_manual);
+ break;
+ case 1:
+ entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_singular);
+ break;
+ default:
+ entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_plural);
+ break;
+
+ }
+ }
+ return entries;
+ }
+
+ private void buildEpisodeCleanupPreference() {
+ final Resources res = ui.getActivity().getResources();
+
+ ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_EPISODE_CLEANUP);
+ String[] values = res.getStringArray(
+ R.array.episode_cleanup_values);
+ String[] entries = new String[values.length];
+ for (int x = 0; x < values.length; x++) {
+ int v = Integer.parseInt(values[x]);
+ if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) {
+ entries[x] = res.getString(R.string.episode_cleanup_queue_removal);
+ } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){
+ entries[x] = res.getString(R.string.episode_cleanup_never);
+ } else if (v == 0) {
+ entries[x] = res.getString(R.string.episode_cleanup_after_listening);
+ } else {
+ entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, v, v);
+ }
+ }
+ pref.setEntries(entries);
+ }
+
+ private void buildSmartMarkAsPlayedPreference() {
+ final Resources res = ui.getActivity().getResources();
+
+ ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS);
+ String[] values = res.getStringArray(R.array.smart_mark_as_played_values);
+ String[] entries = new String[values.length];
+ for (int x = 0; x < values.length; x++) {
+ if(x == 0) {
+ entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled);
+ } else {
+ Integer v = Integer.parseInt(values[x]);
+ if(v < 60) {
+ entries[x] = res.getQuantityString(R.plurals.time_seconds_quantified, v, v);
+ } else {
+ v /= 60;
+ entries[x] = res.getQuantityString(R.plurals.time_minutes_quantified, v, v);
+ }
+ }
+ }
+ pref.setEntries(entries);
+ }
+
+ private void setSelectedNetworksEnabled(boolean b) {
+ if (selectedNetworks != null) {
+ for (Preference p : selectedNetworks) {
+ p.setEnabled(b);
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void checkItemVisibility() {
+ boolean hasFlattrToken = FlattrUtils.hasToken();
+ ui.findPreference(PreferenceController.PREF_FLATTR_SETTINGS).setEnabled(FlattrUtils.hasAPICredentials());
+ ui.findPreference(PreferenceController.PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken);
+ ui.findPreference(PreferenceController.PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken);
+ ui.findPreference(PreferenceController.PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken);
+
+ boolean autoDownload = UserPreferences.isEnableAutodownload();
+ ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(autoDownload);
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(autoDownload);
+ ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(autoDownload);
+ setSelectedNetworksEnabled(autoDownload && UserPreferences.isEnableAutodownloadWifiFilter());
+
+ ui.findPreference(PREF_SEND_CRASH_REPORT).setEnabled(CrashReportWriter.getFile().exists());
+
+ if (Build.VERSION.SDK_INT >= 16) {
+ ui.findPreference(UserPreferences.PREF_SONIC).setEnabled(true);
+ } else {
+ Preference prefSonic = ui.findPreference(UserPreferences.PREF_SONIC);
+ prefSonic.setSummary("[Android 4.1+]\n" + prefSonic.getSummary());
+ }
+ }
+
+ private void setUpdateIntervalText() {
+ Context context = ui.getActivity().getApplicationContext();
+ String val;
+ long interval = UserPreferences.getUpdateInterval();
+ if(interval > 0) {
+ int hours = (int) TimeUnit.MILLISECONDS.toHours(interval);
+ String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours);
+ val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr);
+ } else {
+ int[] timeOfDay = UserPreferences.getUpdateTimeOfDay();
+ if(timeOfDay.length == 2) {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]);
+ cal.set(Calendar.MINUTE, timeOfDay[1]);
+ String timeOfDayStr = DateFormat.getTimeFormat(context).format(cal.getTime());
+ val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_at),
+ timeOfDayStr);
+ } else {
+ val = context.getString(R.string.pref_smart_mark_as_played_disabled);
+ }
+ }
+ String summary = context.getString(R.string.pref_autoUpdateIntervallOrTime_sum) + "\n"
+ + String.format(context.getString(R.string.pref_current_value), val);
+ ui.findPreference(UserPreferences.PREF_UPDATE_INTERVAL).setSummary(summary);
+ }
+
+ private void setParallelDownloadsText(int downloads) {
+ final Resources res = ui.getActivity().getResources();
+
+ String s = Integer.toString(downloads)
+ + res.getString(R.string.parallel_downloads_suffix);
+ ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s);
+ }
+
+ private void setEpisodeCacheSizeText(int cacheSize) {
+ final Resources res = ui.getActivity().getResources();
+
+ String s;
+ if (cacheSize == res.getInteger(
+ R.integer.episode_cache_size_unlimited)) {
+ s = res.getString(R.string.pref_episode_cache_unlimited);
+ } else {
+ s = Integer.toString(cacheSize)
+ + res.getString(R.string.episodes_suffix);
+ }
+ ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s);
+ }
+
+ private void setDataFolderText() {
+ File f = UserPreferences.getDataFolder(null);
+ if (f != null) {
+ ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR)
+ .setSummary(f.getAbsolutePath());
+ }
+ }
+
+ private void buildAutodownloadSelectedNetworsPreference() {
+ final Activity activity = ui.getActivity();
+
+ if (selectedNetworks != null) {
+ clearAutodownloadSelectedNetworsPreference();
+ }
+ // get configured networks
+ WifiManager wifiservice = (WifiManager) activity.getSystemService(Context.WIFI_SERVICE);
+ List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks();
+
+ if (networks != null) {
+ selectedNetworks = new CheckBoxPreference[networks.size()];
+ List<String> prefValues = Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks());
+ PreferenceScreen prefScreen = (PreferenceScreen) ui.findPreference(PreferenceController.AUTO_DL_PREF_SCREEN);
+ Preference.OnPreferenceClickListener clickListener = preference -> {
+ if (preference instanceof CheckBoxPreference) {
+ String key = preference.getKey();
+ List<String> prefValuesList = new ArrayList<>(
+ Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks())
+ );
+ boolean newValue = ((CheckBoxPreference) preference)
+ .isChecked();
+ Log.d(TAG, "Selected network " + key + ". New state: " + newValue);
+
+ int index = prefValuesList.indexOf(key);
+ if (index >= 0 && !newValue) {
+ // remove network
+ prefValuesList.remove(index);
+ } else if (index < 0 && newValue) {
+ prefValuesList.add(key);
+ }
+
+ UserPreferences.setAutodownloadSelectedNetworks(
+ prefValuesList.toArray(new String[prefValuesList.size()])
+ );
+ return true;
+ } else {
+ return false;
+ }
+ };
+ // create preference for each known network. attach listener and set
+ // value
+ for (int i = 0; i < networks.size(); i++) {
+ WifiConfiguration config = networks.get(i);
+
+ CheckBoxPreference pref = new CheckBoxPreference(activity);
+ String key = Integer.toString(config.networkId);
+ pref.setTitle(config.SSID);
+ pref.setKey(key);
+ pref.setOnPreferenceClickListener(clickListener);
+ pref.setPersistent(false);
+ pref.setChecked(prefValues.contains(key));
+ selectedNetworks[i] = pref;
+ prefScreen.addPreference(pref);
+ }
+ } else {
+ Log.e(TAG, "Couldn't get list of configure Wi-Fi networks");
+ }
+ }
+
+ private void clearAutodownloadSelectedNetworsPreference() {
+ if (selectedNetworks != null) {
+ PreferenceScreen prefScreen = (PreferenceScreen) ui.findPreference(PreferenceController.AUTO_DL_PREF_SCREEN);
+
+ for (CheckBoxPreference network : selectedNetworks) {
+ if (network != null) {
+ prefScreen.removePreference(network);
+ }
+ }
+ }
+ }
+
+ private void showDrawerPreferencesDialog() {
+ final Context context = ui.getActivity();
+ final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems();
+ final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles);
+ final String[] NAV_DRAWER_TAGS = MainActivity.NAV_DRAWER_TAGS;
+ boolean[] checked = new boolean[MainActivity.NAV_DRAWER_TAGS.length];
+ for(int i=0; i < NAV_DRAWER_TAGS.length; i++) {
+ String tag = NAV_DRAWER_TAGS[i];
+ if(!hiddenDrawerItems.contains(tag)) {
+ checked[i] = true;
+ }
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.drawer_preferences);
+ builder.setMultiChoiceItems(navTitles, checked, (dialog, which, isChecked) -> {
+ if (isChecked) {
+ hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]);
+ } else {
+ hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]);
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.create().show();
+ }
+
+ private void showNotificationButtonsDialog() {
+ final Context context = ui.getActivity();
+ final List<Integer> preferredButtons = UserPreferences.getCompactNotificationButtons();
+ final String[] allButtonNames = context.getResources().getStringArray(
+ R.array.compact_notification_buttons_options);
+ boolean[] checked = new boolean[allButtonNames.length]; // booleans default to false in java
+
+ for(int i=0; i < checked.length; i++) {
+ if(preferredButtons.contains(i)) {
+ checked[i] = true;
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(String.format(context.getResources().getString(
+ R.string.pref_compact_notification_buttons_dialog_title), 2));
+ builder.setMultiChoiceItems(allButtonNames, checked, (dialog, which, isChecked) -> {
+ checked[which] = isChecked;
+
+ if (isChecked) {
+ if (preferredButtons.size() < 2) {
+ preferredButtons.add(which);
+ } else {
+ // Only allow a maximum of two selections. This is because the notification
+ // on the lock screen can only display 3 buttons, and the play/pause button
+ // is always included.
+ checked[which] = false;
+ ListView selectionView = ((AlertDialog) dialog).getListView();
+ selectionView.setItemChecked(which, false);
+ Snackbar.make(
+ selectionView,
+ String.format(context.getResources().getString(
+ R.string.pref_compact_notification_buttons_dialog_error), 2),
+ Snackbar.LENGTH_SHORT).show();
+ }
+ } else {
+ preferredButtons.remove((Integer) which);
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setCompactNotificationButtons(preferredButtons);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.create().show();
+ }
+
+ // CHOOSE DATA FOLDER
+
+ private void requestPermission() {
+ ActivityCompat.requestPermissions(ui.getActivity(), EXTERNAL_STORAGE_PERMISSIONS,
+ PERMISSION_REQUEST_EXTERNAL_STORAGE);
+ }
+
+ private void openDirectoryChooser() {
+ Activity activity = ui.getActivity();
+ Intent intent = new Intent(activity, DirectoryChooserActivity.class);
+ activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED);
+ }
+
+ private void showChooseDataFolderDialog() {
+ Context context = ui.getActivity();
+ File dataFolder = UserPreferences.getDataFolder(null);
+ if(dataFolder == null) {
+ new MaterialDialog.Builder(ui.getActivity())
+ .title(R.string.error_label)
+ .content(R.string.external_storage_error_msg)
+ .neutralText(android.R.string.ok)
+ .show();
+ return;
+ }
+ String dataFolderPath = dataFolder.getAbsolutePath();
+ int selectedIndex = -1;
+ File[] mediaDirs = ContextCompat.getExternalFilesDirs(context, null);
+ List<String> folders = new ArrayList<>(mediaDirs.length);
+ List<CharSequence> choices = new ArrayList<>(mediaDirs.length);
+ for(int i=0; i < mediaDirs.length; i++) {
+ File dir = mediaDirs[i];
+ if(dir == null || !dir.exists() || !dir.canRead() || !dir.canWrite()) {
+ continue;
+ }
+ String path = mediaDirs[i].getAbsolutePath();
+ folders.add(path);
+ if(dataFolderPath.equals(path)) {
+ selectedIndex = i;
+ }
+ int index = path.indexOf("Android");
+ String choice;
+ if(index >= 0) {
+ choice = path.substring(0, index);
+ } else {
+ choice = path;
+ }
+ long bytes = StorageUtils.getFreeSpaceAvailable(path);
+ String freeSpace = String.format(context.getString(R.string.free_space_label),
+ Converter.byteToString(bytes));
+ choices.add(Html.fromHtml("<html><small>" + choice
+ + " [" + freeSpace + "]" + "</small></html>"));
+ }
+ if(choices.size() == 0) {
+ new MaterialDialog.Builder(ui.getActivity())
+ .title(R.string.error_label)
+ .content(R.string.external_storage_error_msg)
+ .neutralText(android.R.string.ok)
+ .show();
+ return;
+ }
+ MaterialDialog dialog = new MaterialDialog.Builder(ui.getActivity())
+ .title(R.string.choose_data_directory)
+ .content(R.string.choose_data_directory_message)
+ .items(choices.toArray(new CharSequence[choices.size()]))
+ .itemsCallbackSingleChoice(selectedIndex, (dialog1, itemView, which, text) -> {
+ String folder = folders.get(which);
+ Log.d(TAG, "data folder: " + folder);
+ UserPreferences.setDataFolder(folder);
+ setDataFolderText();
+ return true;
+ })
+ .negativeText(R.string.cancel_label)
+ .cancelable(true)
+ .build();
+ dialog.show();
+ }
+
+ // UPDATE TIME/INTERVAL DIALOG
+
+ private void showUpdateIntervalTimePreferencesDialog() {
+ final Context context = ui.getActivity();
+
+ MaterialDialog.Builder builder = new MaterialDialog.Builder(context);
+ builder.title(R.string.pref_autoUpdateIntervallOrTime_title);
+ builder.content(R.string.pref_autoUpdateIntervallOrTime_message);
+ builder.positiveText(R.string.pref_autoUpdateIntervallOrTime_Interval);
+ builder.negativeText(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay);
+ builder.neutralText(R.string.pref_autoUpdateIntervallOrTime_Disable);
+ builder.onPositive((dialog, which) -> {
+ AlertDialog.Builder builder1 = new AlertDialog.Builder(context);
+ builder1.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_Interval));
+ final String[] values = context.getResources().getStringArray(R.array.update_intervall_values);
+ final String[] entries = getUpdateIntervalEntries(values);
+ long currInterval = UserPreferences.getUpdateInterval();
+ int checkedItem = -1;
+ if(currInterval > 0) {
+ String currIntervalStr = String.valueOf(TimeUnit.MILLISECONDS.toHours(currInterval));
+ checkedItem = ArrayUtils.indexOf(values, currIntervalStr);
+ }
+ builder1.setSingleChoiceItems(entries, checkedItem, (dialog1, which1) -> {
+ int hours = Integer.parseInt(values[which1]);
+ UserPreferences.setUpdateInterval(hours);
+ dialog1.dismiss();
+ setUpdateIntervalText();
+ });
+ builder1.setNegativeButton(context.getString(R.string.cancel_label), null);
+ builder1.show();
+ });
+ builder.onNegative((dialog, which) -> {
+ int hourOfDay = 7, minute = 0;
+ int[] updateTime = UserPreferences.getUpdateTimeOfDay();
+ if (updateTime.length == 2) {
+ hourOfDay = updateTime[0];
+ minute = updateTime[1];
+ }
+ TimePickerDialog timePickerDialog = new TimePickerDialog(context,
+ (view, selectedHourOfDay, selectedMinute) -> {
+ if (view.getTag() == null) { // onTimeSet() may get called twice!
+ view.setTag("TAGGED");
+ UserPreferences.setUpdateTimeOfDay(selectedHourOfDay, selectedMinute);
+ setUpdateIntervalText();
+ }
+ }, hourOfDay, minute, DateFormat.is24HourFormat(context));
+ timePickerDialog.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay));
+ timePickerDialog.show();
+ });
+ builder.onNeutral((dialog, which) -> {
+ UserPreferences.setUpdateInterval(0);
+ setUpdateIntervalText();
+ });
+ builder.show();
+ }
+
+
+ public interface PreferenceUI {
+
+ /**
+ * Finds a preference based on its key.
+ */
+ Preference findPreference(CharSequence key);
+
+ Activity getActivity();
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c1b72602a..67d248033 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -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="1060007"
- android:versionName="1.6.0.7">
+ android:versionCode="1060009"
+ android:versionName="1.6.0.9">
<!--
Version code schema:
"1.2.3-SNAPSHOT" -> 1020300
@@ -41,10 +41,6 @@
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
- <meta-data
- android:name="com.google.android.gms.version"
- android:value="@integer/google_play_services_version" />
-
<activity
android:name=".activity.MainActivity"
android:configChanges="keyboardHidden|orientation"
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java
index 1ca4d095f..a7e9b1e70 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java
@@ -50,10 +50,10 @@ public class CastplayerActivity extends MediaplayerInfoActivity {
if (butPlaybackSpeed != null) {
butPlaybackSpeed.setVisibility(View.GONE);
}
- if (butCastDisconnect != null) {
- butCastDisconnect.setOnClickListener(v -> castManager.disconnect());
- butCastDisconnect.setVisibility(View.VISIBLE);
- }
+// if (butCastDisconnect != null) {
+// butCastDisconnect.setOnClickListener(v -> castManager.disconnect());
+// butCastDisconnect.setVisibility(View.VISIBLE);
+// }
}
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
index b2dab7f68..eb6b473d2 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java
@@ -135,7 +135,7 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == CHOOSE_OPML_FILE) {
Uri uri = data.getData();
- if(uri.toString().startsWith("/")) {
+ if(uri != null && uri.toString().startsWith("/")) {
uri = Uri.parse("file://" + uri.toString());
}
importUri(uri);
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
index f22507f4c..81d727283 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/StorageErrorActivity.java
@@ -189,6 +189,9 @@ public class StorageErrorActivity extends AppCompatActivity {
} else {
path = getExternalFilesDir(null);
}
+ if(path == null) {
+ return;
+ }
String message = null;
if(!path.exists()) {
message = String.format(getString(R.string.folder_does_not_exist_error), dir);
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
index a4ffebae2..8531a7356 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -112,7 +112,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected boolean loadMediaInfo() {
- if (!super.loadMediaInfo()) {
+ if (!super.loadMediaInfo() || controller == null) {
return false;
}
Playable media = controller.getMedia();
@@ -152,7 +152,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void onAwaitingVideoSurface() {
- if (videoSurfaceCreated) {
+ if (videoSurfaceCreated && controller != null) {
Log.d(TAG, "Videosurface already created, setting videosurface now");
Pair<Integer, Integer> videoSize = controller.getVideoSize();
@@ -240,7 +240,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "Videoview holder created");
videoSurfaceCreated = true;
- if (controller.getStatus() == PlayerStatus.PLAYING) {
+ if (controller != null && controller.getStatus() == PlayerStatus.PLAYING) {
if (controller.serviceAvailable()) {
controller.setVideoSurface(holder);
} else {
@@ -254,7 +254,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed");
videoSurfaceCreated = false;
- if (!destroyingDueToReload) {
+ if (controller != null && !destroyingDueToReload) {
controller.notifyVideoSurfaceAbandoned();
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
index 66e6f9a00..c18564351 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java
@@ -1,8 +1,9 @@
package de.danoeh.antennapod.adapter;
import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.util.LongList;
public interface ActionButtonCallback {
/** Is called when the action button of a list item has been pressed. */
- void onActionButtonPressed(FeedItem item);
+ void onActionButtonPressed(FeedItem item, LongList queueIds);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java
index d961b548f..3e8bbc488 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java
@@ -232,7 +232,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR
@Override
public void onClick(View v) {
FeedItem item = (FeedItem) v.getTag();
- actionButtonCallback.onActionButtonPressed(item);
+ actionButtonCallback.onActionButtonPressed(item, itemAccess.getQueueIds());
}
};
@@ -319,6 +319,8 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR
boolean isInQueue(FeedItem item);
+ LongList getQueueIds();
+
}
/**
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
index 00ab96f6c..4a53be9dc 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java
@@ -51,13 +51,12 @@ public class DefaultActionButtonCallback implements ActionButtonCallback {
}
@Override
- public void onActionButtonPressed(final FeedItem item) {
+ public void onActionButtonPressed(final FeedItem item, final LongList queueIds) {
if (item.hasMedia()) {
final FeedMedia media = item.getMedia();
boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media);
if (!isDownloading && !media.isDownloaded()) {
- LongList queueIds = DBReader.getQueueIDList();
if (NetworkUtils.isDownloadAllowed() || userAllowedMobileDownloads()) {
try {
DBTasks.downloadFeedItems(context, item);
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
index 4e9c5d71b..07847d0d1 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
@@ -26,6 +26,7 @@ import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.DateUtils;
+import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.ThemeUtils;
/**
@@ -219,7 +220,7 @@ public class FeedItemlistAdapter extends BaseAdapter {
@Override
public void onClick(View v) {
FeedItem item = (FeedItem) v.getTag();
- callback.onActionButtonPressed(item);
+ callback.onActionButtonPressed(item, itemAccess.getQueueIds());
}
};
@@ -243,6 +244,8 @@ public class FeedItemlistAdapter extends BaseAdapter {
FeedItem getItem(int position);
+ LongList getQueueIds();
+
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
index bbd1e0959..c6ddc6c86 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
@@ -309,7 +309,7 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap
@Override
public void onClick(View v) {
FeedItem item = (FeedItem) v.getTag();
- actionButtonCallback.onActionButtonPressed(item);
+ actionButtonCallback.onActionButtonPressed(item, itemAccess.getQueueIds());
}
};
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
index 736988802..3d259c285 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
@@ -102,8 +102,13 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
holder.feedTitle.setText(feed.getTitle());
holder.feedTitle.setVisibility(View.VISIBLE);
- holder.count.setPrimaryText(String.valueOf(itemAccess.getFeedCounter(feed.getId())));
- holder.count.setVisibility(View.VISIBLE);
+ int count = itemAccess.getFeedCounter(feed.getId());
+ if(count > 0) {
+ holder.count.setPrimaryText(String.valueOf(itemAccess.getFeedCounter(feed.getId())));
+ holder.count.setVisibility(View.VISIBLE);
+ } else {
+ holder.count.setVisibility(View.GONE);
+ }
Glide.with(mainActivityRef.get())
.load(feed.getImageLocation())
.error(R.color.light_gray)
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
index 37fb4ca4f..29db19cf8 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java
@@ -380,6 +380,20 @@ public class AllEpisodesFragment extends Fragment {
return item != null && item.isTagged(FeedItem.TAG_QUEUE);
}
+ @Override
+ public LongList getQueueIds() {
+ LongList queueIds = new LongList();
+ if(episodes == null) {
+ return queueIds;
+ }
+ for(FeedItem item : episodes) {
+ if(item.isTagged(FeedItem.TAG_QUEUE)) {
+ queueIds.add(item.getId());
+ }
+ }
+ return queueIds;
+ }
+
};
public void onEventMainThread(FeedItemEvent event) {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
index dd6212b74..509f8b6de 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java
@@ -64,6 +64,7 @@ import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.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.menuhandler.FeedItemMenuHandler;
@@ -578,6 +579,20 @@ public class ItemlistFragment extends ListFragment {
}
@Override
+ public LongList getQueueIds() {
+ LongList queueIds = new LongList();
+ if(feed == null) {
+ return queueIds;
+ }
+ for(FeedItem item : feed.getItems()) {
+ if(item.isTagged(FeedItem.TAG_QUEUE)) {
+ queueIds.add(item.getId());
+ }
+ }
+ return queueIds;
+ }
+
+ @Override
public int getCount() {
return (feed != null) ? feed.getNumOfItems() : 0;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
index 49c68c732..8d40b23d6 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
@@ -28,6 +28,7 @@ import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
+import de.danoeh.antennapod.core.util.LongList;
import de.greenrobot.event.EventBus;
import rx.Observable;
import rx.Subscription;
@@ -251,6 +252,20 @@ public class PlaybackHistoryFragment extends ListFragment {
return null;
}
}
+
+ @Override
+ public LongList getQueueIds() {
+ LongList queueIds = new LongList();
+ if(playbackHistory == null) {
+ return queueIds;
+ }
+ for (FeedItem item : playbackHistory) {
+ if (item.isTagged(FeedItem.TAG_QUEUE)) {
+ queueIds.add(item.getId());
+ }
+ }
+ return queueIds;
+ }
};
private void loadItems() {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
index 08e681c99..ee9390929 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -304,11 +304,11 @@ public class QueueFragment extends Fragment {
};
conDialog.createNewDialog().show();
return true;
- case R.id.queue_sort_alpha_asc:
- QueueSorter.sort(getActivity(), QueueSorter.Rule.ALPHA_ASC, true);
+ case R.id.queue_sort_episode_title_asc:
+ QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_ASC, true);
return true;
- case R.id.queue_sort_alpha_desc:
- QueueSorter.sort(getActivity(), QueueSorter.Rule.ALPHA_DESC, true);
+ case R.id.queue_sort_episode_title_desc:
+ QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_DESC, true);
return true;
case R.id.queue_sort_date_asc:
QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_ASC, true);
@@ -322,6 +322,12 @@ public class QueueFragment extends Fragment {
case R.id.queue_sort_duration_desc:
QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_DESC, true);
return true;
+ case R.id.queue_sort_feed_title_asc:
+ QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_ASC, true);
+ return true;
+ case R.id.queue_sort_feed_title_desc:
+ QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_DESC, true);
+ return true;
default:
return false;
}
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
index b80213459..6799114f7 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -80,7 +80,7 @@ public class FeedItemMenuHandler {
if(queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId()) {
mi.setItemVisibility(R.id.move_to_bottom_item, false);
}
- if (!isInQueue || isPlaying) {
+ if (!isInQueue) {
mi.setItemVisibility(R.id.remove_from_queue_item, false);
}
if (!(!isInQueue && selectedItem.getMedia() != null)) {
diff --git a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java
index 323060f81..007a4d744 100644
--- a/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java
+++ b/app/src/main/java/de/danoeh/antennapod/service/PlayerWidgetService.java
@@ -196,9 +196,10 @@ public class PlayerWidgetService extends Service {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Connection to service established");
synchronized (psLock) {
- playbackService = ((PlaybackService.LocalBinder) service)
- .getService();
- startViewUpdaterIfNotRunning();
+ if(service instanceof PlaybackService.LocalBinder) {
+ playbackService = ((PlaybackService.LocalBinder) service).getService();
+ startViewUpdaterIfNotRunning();
+ }
}
}
diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml
index 01a11b10e..a5fe85865 100644
--- a/app/src/main/res/menu/queue.xml
+++ b/app/src/main/res/menu/queue.xml
@@ -36,47 +36,60 @@
<menu>
<item
- android:id="@+id/queue_sort_alpha"
- android:title="@string/alpha">
+ android:id="@+id/queue_sort_date"
+ android:title="@string/date">
<menu>
<item
- android:id="@+id/queue_sort_alpha_asc"
+ android:id="@+id/queue_sort_date_asc"
android:title="@string/ascending"/>
<item
- android:id="@+id/queue_sort_alpha_desc"
+ android:id="@+id/queue_sort_date_desc"
android:title="@string/descending"/>
</menu>
</item>
<item
- android:id="@+id/queue_sort_date"
- android:title="@string/date">
+ android:id="@+id/queue_sort_duration"
+ android:title="@string/duration">
<menu>
<item
- android:id="@+id/queue_sort_date_asc"
+ android:id="@+id/queue_sort_duration_asc"
android:title="@string/ascending"/>
<item
- android:id="@+id/queue_sort_date_desc"
+ android:id="@+id/queue_sort_duration_desc"
android:title="@string/descending"/>
</menu>
</item>
<item
- android:id="@+id/queue_sort_duration"
- android:title="@string/duration">
+ android:id="@+id/queue_sort_episode_title"
+ android:title="@string/episode_title">
<menu>
<item
- android:id="@+id/queue_sort_duration_asc"
+ android:id="@+id/queue_sort_episode_title_asc"
android:title="@string/ascending"/>
<item
- android:id="@+id/queue_sort_duration_desc"
+ android:id="@+id/queue_sort_episode_title_desc"
android:title="@string/descending"/>
</menu>
</item>
+ <item
+ android:id="@+id/queue_sort_feed_title"
+ android:title="@string/feed_title">
+
+ <menu>
+ <item
+ android:id="@+id/queue_sort_feed_title_asc"
+ android:title="@string/ascending"/>
+ <item
+ android:id="@+id/queue_sort_feed_title_desc"
+ android:title="@string/descending"/>
+ </menu>
+ </item>
</menu>
</item>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..a244c494f
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name" translate="false">AntennaPod</string>
+</resources>
diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml
new file mode 100644
index 000000000..dcf82e44a
--- /dev/null
+++ b/app/src/play/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="de.danoeh.antennapod">
+
+ <application>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+ </application>
+
+</manifest>
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
index b8856c295..b8856c295 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
+++ b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/MainActivity.java
index b7c7d86c7..17965ca8e 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/play/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -209,7 +209,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
} else {
edit.remove(PREF_LAST_FRAGMENT_TAG);
}
- edit.commit();
+ edit.apply();
}
private String getLastNavFragment() {
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
index 71d288725..71d288725 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ b/app/src/play/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/play/java/de/danoeh/antennapod/fragment/ItemFragment.java
index 798e6c198..524b78ed2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/play/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -59,6 +59,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
@@ -220,7 +221,8 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
return;
}
DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getActivity());
- actionButtonCallback.onActionButtonPressed(item);
+ actionButtonCallback.onActionButtonPressed(item, item.isTagged(FeedItem.TAG_QUEUE) ?
+ LongList.of(item.getId()) : new LongList(0));
FeedMedia media = item.getMedia();
if (media != null && media.isDownloaded()) {
// playback was started, dialog should close itself
diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceController.java
index 183ef338a..183ef338a 100644
--- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java
+++ b/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceController.java
diff --git a/core/build.gradle b/core/build.gradle
index 31042456b..fa95800c2 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -30,6 +30,14 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
+ publishNonDefault true
+ productFlavors {
+ free {
+ }
+ play {
+ }
+ }
+
}
repositories {
@@ -59,9 +67,9 @@ dependencies {
compile "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
// Add casting features
- compile "com.google.android.libraries.cast.companionlibrary:ccl:$castCompanionLibVer"
+ playCompile "com.google.android.libraries.cast.companionlibrary:ccl:$castCompanionLibVer"
compile "com.android.support:mediarouter-v7:$supportVersion"
- compile "com.google.android.gms:play-services-cast:$playServicesVersion"
+ playCompile "com.google.android.gms:play-services-cast:$playServicesVersion"
}
allprojects {
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
new file mode 100644
index 000000000..d1c93d782
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -0,0 +1,48 @@
+package de.danoeh.antennapod.core;
+
+import android.content.Context;
+
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+
+/**
+ * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables.
+ * Apps using the core module of AntennaPod should register implementations of all interfaces here.
+ */
+public class ClientConfig {
+
+ /**
+ * Should be used when setting User-Agent header for HTTP-requests.
+ */
+ public static String USER_AGENT;
+
+ public static ApplicationCallbacks applicationCallbacks;
+
+ public static DownloadServiceCallbacks downloadServiceCallbacks;
+
+ public static PlaybackServiceCallbacks playbackServiceCallbacks;
+
+ public static GpodnetCallbacks gpodnetCallbacks;
+
+ public static FlattrCallbacks flattrCallbacks;
+
+ public static DBTasksCallbacks dbTasksCallbacks;
+
+ private static boolean initialized = false;
+
+ public static synchronized void initialize(Context context) {
+ if(initialized) {
+ return;
+ }
+ PodDBAdapter.init(context);
+ UserPreferences.init(context);
+ UpdateManager.init(context);
+ PlaybackPreferences.init(context);
+ NetworkUtils.init(context);
+// CastManager.init(context);
+ initialized = true;
+ }
+
+}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/free/java/de/danoeh/antennapod/core/feed/FeedMedia.java
new file mode 100644
index 000000000..cde66835a
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -0,0 +1,567 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+public class FeedMedia extends FeedFile implements Playable {
+ private static final String TAG = "FeedMedia";
+
+ public static final int FEEDFILETYPE_FEEDMEDIA = 2;
+ public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
+
+ public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
+ public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
+
+ /**
+ * Indicates we've checked on the size of the item via the network
+ * and got an invalid response. Using Integer.MIN_VALUE because
+ * 1) we'll still check on it in case it gets downloaded (it's <= 0)
+ * 2) By default all FeedMedia have a size of 0 if we don't know it,
+ * so this won't conflict with existing practice.
+ */
+ private static final int CHECKED_ON_SIZE_BUT_UNKNOWN = Integer.MIN_VALUE;
+
+ private int duration;
+ private int position; // Current position in file
+ private long lastPlayedTime; // Last time this media was played (in ms)
+ private int played_duration; // How many ms of this file have been played (for autoflattring)
+ private long size; // File size in Byte
+ private String mime_type;
+ @Nullable private volatile FeedItem item;
+ private Date playbackCompletionDate;
+
+ // if null: unknown, will be checked
+ private Boolean hasEmbeddedPicture;
+
+ /* Used for loading item when restoring from parcel. */
+ private long itemID;
+
+ public FeedMedia(FeedItem i, String download_url, long size,
+ String mime_type) {
+ super(null, download_url, false);
+ this.item = i;
+ this.size = size;
+ this.mime_type = mime_type;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position,
+ long size, String mime_type, String file_url, String download_url,
+ boolean downloaded, Date playbackCompletionDate, int played_duration,
+ long lastPlayedTime) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.item = item;
+ this.duration = duration;
+ this.position = position;
+ this.played_duration = played_duration;
+ this.size = size;
+ this.mime_type = mime_type;
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position,
+ long size, String mime_type, String file_url, String download_url,
+ boolean downloaded, Date playbackCompletionDate, int played_duration,
+ Boolean hasEmbeddedPicture, long lastPlayedTime) {
+ this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded,
+ playbackCompletionDate, played_duration, lastPlayedTime);
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
+ public static FeedMedia fromCursor(Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexPlaybackCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE);
+ int indexDuration = cursor.getColumnIndex(PodDBAdapter.KEY_DURATION);
+ int indexPosition = cursor.getColumnIndex(PodDBAdapter.KEY_POSITION);
+ int indexSize = cursor.getColumnIndex(PodDBAdapter.KEY_SIZE);
+ int indexMimeType = cursor.getColumnIndex(PodDBAdapter.KEY_MIME_TYPE);
+ int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
+ int indexPlayedDuration = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYED_DURATION);
+ int indexLastPlayedTime = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_PLAYED_TIME);
+
+ long mediaId = cursor.getLong(indexId);
+ Date playbackCompletionDate = null;
+ long playbackCompletionTime = cursor.getLong(indexPlaybackCompletionDate);
+ if (playbackCompletionTime > 0) {
+ playbackCompletionDate = new Date(playbackCompletionTime);
+ }
+
+ Boolean hasEmbeddedPicture;
+ switch(cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) {
+ case 1:
+ hasEmbeddedPicture = Boolean.TRUE;
+ break;
+ case 0:
+ hasEmbeddedPicture = Boolean.FALSE;
+ break;
+ default:
+ hasEmbeddedPicture = null;
+ break;
+ }
+
+ return new FeedMedia(
+ mediaId,
+ null,
+ cursor.getInt(indexDuration),
+ cursor.getInt(indexPosition),
+ cursor.getLong(indexSize),
+ cursor.getString(indexMimeType),
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ playbackCompletionDate,
+ cursor.getInt(indexPlayedDuration),
+ hasEmbeddedPicture,
+ cursor.getLong(indexLastPlayedTime)
+ );
+ }
+
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (item != null && item.getTitle() != null) {
+ return item.getTitle();
+ } else {
+ return download_url;
+ }
+ }
+
+ /**
+ * Uses mimetype to determine the type of media.
+ */
+ public MediaType getMediaType() {
+ return MediaType.fromMimeType(mime_type);
+ }
+
+ public void updateFromOther(FeedMedia other) {
+ super.updateFromOther(other);
+ if (other.size > 0) {
+ size = other.size;
+ }
+ if (other.mime_type != null) {
+ mime_type = other.mime_type;
+ }
+ }
+
+ public boolean compareWithOther(FeedMedia other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (other.mime_type != null) {
+ if (mime_type == null || !mime_type.equals(other.mime_type)) {
+ return true;
+ }
+ }
+ if (other.size > 0 && other.size != size) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Reads playback preferences to determine whether this FeedMedia object is
+ * currently being played.
+ */
+ public boolean isPlaying() {
+ return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA
+ && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id;
+ }
+
+ /**
+ * Reads playback preferences to determine whether this FeedMedia object is
+ * currently being played and the current player status is playing.
+ */
+ public boolean isCurrentlyPlaying() {
+ return isPlaying() &&
+ ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING));
+ }
+
+ /**
+ * Reads playback preferences to determine whether this FeedMedia object is
+ * currently being played and the current player status is paused.
+ */
+ public boolean isCurrentlyPaused() {
+ return isPlaying() &&
+ ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PAUSED));
+ }
+
+
+ public boolean hasAlmostEnded() {
+ int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs();
+ return this.position >= this.duration - smartMarkAsPlayedSecs * 1000;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEEDMEDIA;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ @Override
+ public void setLastPlayedTime(long lastPlayedTime) {
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
+ public int getPlayedDuration() {
+ return played_duration;
+ }
+
+ public void setPlayedDuration(int played_duration) {
+ this.played_duration = played_duration;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ if(position > 0 && item != null && item.isNew()) {
+ this.item.setPlayed(false);
+ }
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ /**
+ * Indicates we asked the service what the size was, but didn't
+ * get a valid answer and we shoudln't check using the network again.
+ */
+ public void setCheckedOnSizeButUnknown() {
+ this.size = CHECKED_ON_SIZE_BUT_UNKNOWN;
+ }
+
+ public boolean checkedOnSizeButUnknown() {
+ return (CHECKED_ON_SIZE_BUT_UNKNOWN == this.size);
+ }
+
+ public String getMime_type() {
+ return mime_type;
+ }
+
+ public void setMime_type(String mime_type) {
+ this.mime_type = mime_type;
+ }
+
+ @Nullable
+ public FeedItem getItem() {
+ return item;
+ }
+
+ /**
+ * Sets the item object of this FeedMedia. If the given
+ * FeedItem object is not null, it's 'media'-attribute value
+ * will also be set to this media object.
+ */
+ public void setItem(FeedItem item) {
+ this.item = item;
+ if (item != null && item.getMedia() != this) {
+ item.setMedia(this);
+ }
+ }
+
+ public Date getPlaybackCompletionDate() {
+ return playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public void setPlaybackCompletionDate(Date playbackCompletionDate) {
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public boolean isInProgress() {
+ return (this.position > 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public boolean hasEmbeddedPicture() {
+ if(hasEmbeddedPicture == null) {
+ checkEmbeddedPicture();
+ }
+ return hasEmbeddedPicture;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeLong(item != null ? item.getId() : 0L);
+
+ dest.writeInt(duration);
+ dest.writeInt(position);
+ dest.writeLong(size);
+ dest.writeString(mime_type);
+ dest.writeString(file_url);
+ dest.writeString(download_url);
+ dest.writeByte((byte) ((downloaded) ? 1 : 0));
+ dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0);
+ dest.writeInt(played_duration);
+ dest.writeLong(lastPlayedTime);
+ }
+
+ @Override
+ public void writeToPreferences(Editor prefEditor) {
+ if(item != null && item.getFeed() != null) {
+ prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
+ } else {
+ prefEditor.putLong(PREF_FEED_ID, 0L);
+ }
+ prefEditor.putLong(PREF_MEDIA_ID, id);
+ }
+
+ @Override
+ public void loadMetadata() throws PlayableException {
+ if (item == null && itemID != 0) {
+ item = DBReader.getFeedItem(itemID);
+ }
+ }
+
+ @Override
+ public void loadChapterMarks() {
+ if (item == null && itemID != 0) {
+ item = DBReader.getFeedItem(itemID);
+ }
+ // check if chapters are stored in db and not loaded yet.
+ if (item != null && item.hasChapters() && item.getChapters() == null) {
+ DBReader.loadChaptersOfFeedItem(item);
+ } else if (item != null && item.getChapters() == null) {
+ if(localFileAvailable()) {
+ ChapterUtils.loadChaptersFromFileUrl(this);
+ } else {
+ ChapterUtils.loadChaptersFromStreamUrl(this);
+ }
+ if (getChapters() != null && item != null) {
+ DBWriter.setFeedItem(item);
+ }
+ }
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ if (item == null) {
+ return null;
+ }
+ if (item.getTitle() != null) {
+ return item.getTitle();
+ } else {
+ return item.getIdentifyingValue();
+ }
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ if (item == null) {
+ return null;
+ }
+ return item.getChapters();
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ if (item == null) {
+ return null;
+ }
+ return item.getLink();
+ }
+
+ @Override
+ public String getFeedTitle() {
+ if (item == null || item.getFeed() == null) {
+ return null;
+ }
+ return item.getFeed().getTitle();
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return id;
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return file_url;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return download_url;
+ }
+
+ @Override
+ public String getPaymentLink() {
+ if (item == null) {
+ return null;
+ }
+ return item.getPaymentLink();
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return isDownloaded() && file_url != null;
+ }
+
+ @Override
+ public boolean streamAvailable() {
+ return download_url != null;
+ }
+
+ @Override
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) {
+ if(item != null && item.isNew()) {
+ DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
+ }
+ setPosition(newPosition);
+ setLastPlayedTime(timeStamp);
+ DBWriter.setFeedMediaPlaybackInformation(this);
+ }
+
+ @Override
+ public void onPlaybackStart() {
+ }
+ @Override
+ public void onPlaybackCompleted() {
+
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_FEEDMEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ if(item != null) {
+ item.setChapters(chapters);
+ }
+ }
+
+ @Override
+ public Callable<String> loadShownotes() {
+ return () -> {
+ if (item == null) {
+ item = DBReader.getFeedItem(
+ itemID);
+ }
+ if (item.getContentEncoded() == null || item.getDescription() == null) {
+ DBReader.loadExtraInformationOfFeedItem(
+ item);
+
+ }
+ return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription();
+ };
+ }
+
+ public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() {
+ public FeedMedia createFromParcel(Parcel in) {
+ final long id = in.readLong();
+ final long itemID = in.readLong();
+ FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(),
+ in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt(), in.readLong());
+ result.itemID = itemID;
+ return result;
+ }
+
+ public FeedMedia[] newArray(int size) {
+ return new FeedMedia[size];
+ }
+ };
+
+ @Override
+ public String getImageLocation() {
+ if (hasEmbeddedPicture()) {
+ return getLocalMediaUrl();
+ } else if(item != null) {
+ return item.getImageLocation();
+ } else {
+ return null;
+ }
+ }
+
+ public void setHasEmbeddedPicture(Boolean hasEmbeddedPicture) {
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
+ @Override
+ public void setDownloaded(boolean downloaded) {
+ super.setDownloaded(downloaded);
+ if(item != null && downloaded) {
+ item.setPlayed(false);
+ }
+ }
+
+ @Override
+ public void setFile_url(String file_url) {
+ super.setFile_url(file_url);
+ }
+
+ public void checkEmbeddedPicture() {
+ if (!localFileAvailable()) {
+ hasEmbeddedPicture = Boolean.FALSE;
+ return;
+ }
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ try {
+ mmr.setDataSource(getLocalMediaUrl());
+ byte[] image = mmr.getEmbeddedPicture();
+ if(image != null) {
+ hasEmbeddedPicture = Boolean.TRUE;
+ } else {
+ hasEmbeddedPicture = Boolean.FALSE;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ hasEmbeddedPicture = Boolean.FALSE;
+ }
+ }
+
+// @Override
+// public boolean equals(Object o) {
+// if (o instanceof RemoteMedia) {
+// return o.equals(this);
+// }
+// return super.equals(o);
+// }
+}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
new file mode 100644
index 000000000..01b803d80
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -0,0 +1,1773 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.bluetooth.BluetoothA2dp;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v7.app.NotificationCompat;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.Target;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+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.gpoddernet.model.GpodnetEpisodeAction;
+import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
+import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.IntList;
+import de.danoeh.antennapod.core.util.QueueAccess;
+import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Controls the MediaPlayer that plays a FeedMedia-file
+ */
+public class PlaybackService extends Service {
+ public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
+ public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE";
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "PlaybackService";
+
+ /**
+ * Parcelable of type Playable.
+ */
+ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
+ /**
+ * True if cast session should disconnect.
+ */
+ public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
+ /**
+ * True if media should be streamed.
+ */
+ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
+ /**
+ * True if playback should be started immediately after media has been
+ * prepared.
+ */
+ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
+
+ public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
+
+ public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged";
+ public static final String EXTRA_NEW_PLAYER_STATUS = "extra.de.danoeh.antennapod.service.playerStatusChanged.newStatus";
+ private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged";
+ private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged";
+
+ public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification";
+ public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode";
+ public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType";
+
+ /**
+ * If the PlaybackService receives this action, it will stop playback and
+ * try to shutdown.
+ */
+ public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService";
+
+ /**
+ * If the PlaybackService receives this action, it will end playback of the
+ * current episode and load the next episode if there is one available.
+ */
+ public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode";
+
+ /**
+ * If the PlaybackService receives this action, it will pause playback.
+ */
+ public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode";
+
+
+ /**
+ * If the PlaybackService receives this action, it will resume playback.
+ */
+ public static final String ACTION_RESUME_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.resumePlayCurrentEpisode";
+
+
+ /**
+ * Used in NOTIFICATION_TYPE_RELOAD.
+ */
+ public static final int EXTRA_CODE_AUDIO = 1;
+ public static final int EXTRA_CODE_VIDEO = 2;
+ public static final int EXTRA_CODE_CAST = 3;
+
+ public static final int NOTIFICATION_TYPE_ERROR = 0;
+ public static final int NOTIFICATION_TYPE_INFO = 1;
+ public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
+
+ /**
+ * Receivers of this intent should update their information about the curently playing media
+ */
+ public static final int NOTIFICATION_TYPE_RELOAD = 3;
+ /**
+ * The state of the sleeptimer changed.
+ */
+ public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4;
+ public static final int NOTIFICATION_TYPE_BUFFER_START = 5;
+ public static final int NOTIFICATION_TYPE_BUFFER_END = 6;
+ /**
+ * No more episodes are going to be played.
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7;
+
+ /**
+ * Playback speed has changed
+ */
+ public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8;
+
+ /**
+ * Ability to set the playback speed has changed
+ */
+ public static final int NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED = 9;
+
+ /**
+ * Send a message to the user (with provided String resource id)
+ */
+ public static final int NOTIFICATION_TYPE_SHOW_TOAST = 10;
+
+ /**
+ * Returned by getPositionSafe() or getDurationSafe() if the playbackService
+ * is in an invalid state.
+ */
+ public static final int INVALID_TIME = -1;
+
+ /**
+ * Time in seconds during which the CastManager will try to reconnect to the Cast Device after
+ * the Wifi Connection is regained.
+ */
+ private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15;
+
+ /**
+ * Is true if service is running.
+ */
+ public static boolean isRunning = false;
+ /**
+ * Is true if service has received a valid start command.
+ */
+ public static boolean started = false;
+ /**
+ * Is true if the service was running, but paused due to headphone disconnect
+ */
+ public static boolean transientPause = false;
+ /**
+ * Is true if a Cast Device is connected to the service.
+ */
+ private static volatile boolean isCasting = false;
+ /**
+ * Stores the state of the cast playback just before it disconnects.
+ */
+ private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection;
+
+ private boolean wifiConnectivity = true;
+ private BroadcastReceiver wifiBroadcastReceiver;
+
+ private static final int NOTIFICATION_ID = 1;
+
+ private PlaybackServiceMediaPlayer mediaPlayer;
+ private PlaybackServiceTaskManager taskManager;
+
+// private CastManager castManager;
+// private MediaRouter mediaRouter;
+ /**
+ * Only used for Lollipop notifications.
+ */
+ private MediaSessionCompat mediaSession;
+
+ private int startPosition;
+
+ private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public PlaybackService getService() {
+ return PlaybackService.this;
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ Log.d(TAG, "Received onUnbind event");
+ return super.onUnbind(intent);
+ }
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played. If the playbackservice is not
+ * running, the type of the last played media will be looked up.
+ */
+ public static Intent getPlayerActivityIntent(Context context) {
+ if (isRunning) {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting);
+ } else {
+ if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting);
+ } else {
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting);
+ }
+ }
+ }
+
+ /**
+ * Same as getPlayerActivityIntent(context), but here the type of activity
+ * depends on the FeedMedia that is provided as an argument.
+ */
+ public static Intent getPlayerActivityIntent(Context context, Playable media) {
+ MediaType mt = media.getMediaType();
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "Service created.");
+ isRunning = true;
+
+ registerReceiver(headsetDisconnected, new IntentFilter(
+ Intent.ACTION_HEADSET_PLUG));
+ registerReceiver(shutdownReceiver, new IntentFilter(
+ ACTION_SHUTDOWN_PLAYBACK_SERVICE));
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ registerReceiver(bluetoothStateUpdated, new IntentFilter(
+ BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED));
+ }
+ registerReceiver(audioBecomingNoisy, new IntentFilter(
+ AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_SKIP_CURRENT_EPISODE));
+ registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_PAUSE_PLAY_CURRENT_EPISODE));
+ registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter(
+ ACTION_RESUME_PLAY_CURRENT_EPISODE));
+ taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
+
+// mediaRouter = MediaRouter.getInstance(getApplicationContext());
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .registerOnSharedPreferenceChangeListener(prefListener);
+
+ ComponentName eventReceiver = new ComponentName(getApplicationContext(),
+ MediaButtonReceiver.class);
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(eventReceiver);
+ PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent);
+
+ try {
+ mediaSession.setCallback(sessionCallback);
+ mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ } catch (NullPointerException npe) {
+ // on some devices (Huawei) setting active can cause a NullPointerException
+ // even with correct use of the api.
+ // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat
+ // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d
+ Log.e(TAG, "NullPointerException while setting up MediaSession");
+ npe.printStackTrace();
+ }
+
+// castManager = CastManager.getInstance();
+// castManager.addCastConsumer(castConsumer);
+// isCasting = castManager.isConnected();
+// if (isCasting) {
+// if (UserPreferences.isCastEnabled()) {
+// onCastAppConnected(false);
+// } else {
+// castManager.disconnect();
+// }
+// } else {
+ mediaPlayer = new LocalPSMP(this, mediaPlayerCallback);
+// }
+
+ mediaSession.setActive(true);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(TAG, "Service is about to be destroyed");
+ isRunning = false;
+ started = false;
+ currentMediaType = MediaType.UNKNOWN;
+
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .unregisterOnSharedPreferenceChangeListener(prefListener);
+ if (mediaSession != null) {
+ mediaSession.release();
+ }
+ unregisterReceiver(headsetDisconnected);
+ unregisterReceiver(shutdownReceiver);
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ unregisterReceiver(bluetoothStateUpdated);
+ }
+ unregisterReceiver(audioBecomingNoisy);
+ unregisterReceiver(skipCurrentEpisodeReceiver);
+ unregisterReceiver(pausePlayCurrentEpisodeReceiver);
+ unregisterReceiver(pauseResumeCurrentEpisodeReceiver);
+// castManager.removeCastConsumer(castConsumer);
+ unregisterWifiBroadcastReceiver();
+ mediaPlayer.shutdown();
+ taskManager.shutdown();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.d(TAG, "Received onBind event");
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ Log.d(TAG, "OnStartCommand called");
+ final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
+ final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
+ if (keycode == -1 && playable == null && !castDisconnect) {
+ Log.e(TAG, "PlaybackService was started with no arguments");
+ stopSelf();
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
+ Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
+ stopForeground(true);
+ } else {
+
+ if (keycode != -1) {
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE,
+ InputDevice.SOURCE_CLASS_NONE));
+// } else if (castDisconnect) {
+// castManager.disconnect();
+ } else {
+ started = true;
+ boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
+ boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ //If the user asks to play External Media, the casting session, if on, should end.
+// if (playable instanceof ExternalMedia) {
+// castManager.disconnect();
+// }
+ mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
+ }
+ }
+
+ return Service.START_REDELIVER_INTENT;
+ }
+
+ /**
+ * Handles media button events
+ */
+ private void handleKeycode(int keycode, int source) {
+ Log.d(TAG, "Handling keycode: " + keycode);
+ final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ final PlayerStatus status = info.playerStatus;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
+ } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.PREPARING) {
+ mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared());
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ mediaPlayer.resume();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ mediaPlayer.setStartWhenPrepared(true);
+ mediaPlayer.prepare();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
+ }
+
+ break;
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ if(source == InputDevice.SOURCE_CLASS_NONE ||
+ UserPreferences.shouldHardwareButtonSkip()) {
+ // assume the skip command comes from a notification or the lockscreen
+ // a >| skip button should actually skip
+ mediaPlayer.endPlayback(true, false);
+ } else {
+ // assume skip command comes from a (bluetooth) media button
+ // user actually wants to fast-forward
+ seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ break;
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ if (status == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, true);
+ started = false;
+ }
+
+ stopForeground(true); // gets rid of persistent notification
+ break;
+ default:
+ Log.d(TAG, "Unhandled key code: " + keycode);
+ if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something
+ String message = String.format(getResources().getString(R.string.unknown_media_key), keycode);
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Called by a mediaplayer Activity as soon as it has prepared its
+ * mediaplayer.
+ */
+ public void setVideoSurface(SurfaceHolder sh) {
+ Log.d(TAG, "Setting display");
+ mediaPlayer.setVideoSurface(sh);
+ }
+
+ /**
+ * Called when the surface holder of the mediaplayer has to be changed.
+ */
+ private void resetVideoSurface() {
+ taskManager.cancelPositionSaver();
+ mediaPlayer.resetVideoSurface();
+ }
+
+ public void notifyVideoSurfaceAbandoned() {
+ stopForeground(!UserPreferences.isPersistNotify());
+ mediaPlayer.resetVideoSurface();
+ }
+
+ private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
+ @Override
+ public void positionSaverTick() {
+ saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL);
+ }
+
+ @Override
+ public void onSleepTimerAlmostExpired() {
+ float leftVolume = 0.1f * UserPreferences.getLeftVolume();
+ float rightVolume = 0.1f * UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ }
+
+ @Override
+ public void onSleepTimerExpired() {
+ mediaPlayer.pause(true, true);
+ float leftVolume = UserPreferences.getLeftVolume();
+ float rightVolume = UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ @Override
+ public void onSleepTimerReset() {
+ float leftVolume = UserPreferences.getLeftVolume();
+ float rightVolume = UserPreferences.getRightVolume();
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ }
+
+ @Override
+ public void onWidgetUpdaterTick() {
+ updateWidget();
+ }
+
+ @Override
+ public void onChapterLoaded(Playable media) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ };
+
+ private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ currentMediaType = mediaPlayer.getCurrentMediaType();
+ updateMediaSession(newInfo.playerStatus);
+ switch (newInfo.playerStatus) {
+ case INITIALIZED:
+ writePlaybackPreferences();
+ break;
+
+ case PREPARED:
+ taskManager.startChapterLoader(newInfo.playable);
+ break;
+
+ case PAUSED:
+ taskManager.cancelPositionSaver();
+ saveCurrentPosition(false, 0);
+ taskManager.cancelWidgetUpdater();
+ if ((UserPreferences.isPersistNotify() || isCasting) &&
+ android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ // do not remove notification on pause based on user pref and whether android version supports expanded notifications
+ // Change [Play] button to [Pause]
+ setupNotification(newInfo);
+ } else if (!UserPreferences.isPersistNotify() && !isCasting) {
+ // remove notification on pause
+ stopForeground(true);
+ }
+ writePlayerStatusPlaybackPreferences();
+
+ final Playable playable = newInfo.playable;
+
+ // Gpodder: send play action
+ if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) playable;
+ FeedItem item = media.getItem();
+ GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
+ .currentDeviceId()
+ .currentTimestamp()
+ .started(startPosition / 1000)
+ .position(getCurrentPosition() / 1000)
+ .total(getDuration() / 1000)
+ .build();
+ GpodnetPreferences.enqueueEpisodeAction(action);
+ }
+ break;
+
+ case STOPPED:
+ //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING);
+ //stopSelf();
+ break;
+
+ case PLAYING:
+ Log.d(TAG, "Audiofocus successfully requested");
+ Log.d(TAG, "Resuming/Starting playback");
+
+ taskManager.startPositionSaver();
+ taskManager.startWidgetUpdater();
+ writePlayerStatusPlaybackPreferences();
+ setupNotification(newInfo);
+ started = true;
+ startPosition = mediaPlayer.getPosition();
+ break;
+
+ case ERROR:
+ writePlaybackPreferencesNoMediaPlaying();
+ break;
+
+ }
+
+ Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED);
+ // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
+ sendBroadcast(statusUpdate);
+ updateWidget();
+ bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
+ bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
+ }
+
+ @Override
+ public void shouldStop() {
+ stopSelf();
+ }
+
+ @Override
+ public void playbackSpeedChanged(float s) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0);
+ }
+
+ public void setSpeedAbilityChanged() {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED, 0);
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+ }
+
+ @Override
+ public void onMediaChanged(boolean reloadUI) {
+ Log.d(TAG, "reloadUI callback reached");
+ if (reloadUI) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ }
+ PlaybackService.this.updateMediaSessionMetadata(getPlayable());
+ }
+
+ @Override
+ public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
+ switch (code) {
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
+ return true;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
+ return true;
+// case RemotePSMP.CAST_ERROR:
+// sendNotificationBroadcast(NOTIFICATION_TYPE_SHOW_TOAST, resourceId);
+// return true;
+// case RemotePSMP.CAST_ERROR_PRIORITY_HIGH:
+// Toast.makeText(PlaybackService.this, resourceId, Toast.LENGTH_SHORT).show();
+// return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onMediaPlayerError(Object inObj, int what, int extra) {
+ final String TAG = "PlaybackSvc.onErrorLtsn";
+ Log.w(TAG, "An error has occured: " + what + " " + extra);
+ if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
+ mediaPlayer.pause(true, false);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ writePlaybackPreferencesNoMediaPlaying();
+ stopSelf();
+ return true;
+ }
+
+ @Override
+ public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
+ PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers);
+ return true;
+ }
+ };
+
+ private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
+ Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": ""));
+
+ if (playable == null) {
+ Log.e(TAG, "Cannot end playback: media was null");
+ return;
+ }
+
+ taskManager.cancelPositionSaver();
+
+ boolean isInQueue = false;
+ FeedItem nextItem = null;
+
+ if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) {
+ FeedMedia media = (FeedMedia) playable;
+ FeedItem item = media.getItem();
+
+ if (!switchingPlayers) {
+ try {
+ final List<FeedItem> queue = taskManager.getQueue();
+ isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId());
+ nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ // isInQueue remains false
+ }
+
+ boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode();
+
+ if (!shouldKeep) {
+ // only mark the item as played if we're not keeping it anyways
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
+
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item, true);
+ }
+
+ // Delete episode if enabled
+ if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
+ DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
+ Log.d(TAG, "Episode Deleted");
+ }
+ }
+ }
+
+
+ DBWriter.addItemToPlaybackHistory(media);
+
+ // auto-flattr if enabled
+ if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
+ DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
+ }
+
+ // gpodder play action
+ if(GpodnetPreferences.loggedIn()) {
+ GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
+ .currentDeviceId()
+ .currentTimestamp()
+ .started(startPosition / 1000)
+ .position(getDuration() / 1000)
+ .total(getDuration() / 1000)
+ .build();
+ GpodnetPreferences.enqueueEpisodeAction(action);
+ }
+ }
+
+ if (!switchingPlayers) {
+ // Load next episode if previous episode was in the queue and if there
+ // is an episode in the queue left.
+ // Start playback immediately if continuous playback is enabled
+ Playable nextMedia = null;
+ boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() &&
+ isInQueue &&
+ nextItem != null;
+
+ playNextEpisode = playNextEpisode &&
+ loadNextItem &&
+ UserPreferences.isFollowQueue();
+
+ if (loadNextItem) {
+ Log.d(TAG, "Loading next item in queue");
+ nextMedia = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ final boolean startWhenPrepared;
+ final boolean stream;
+
+ if (playNextEpisode) {
+ Log.d(TAG, "Playback of next episode will start immediately.");
+ prepareImmediately = startWhenPrepared = true;
+ } else {
+ Log.d(TAG, "No more episodes available to play");
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
+
+ writePlaybackPreferencesNoMediaPlaying();
+ if (nextMedia != null) {
+ stream = !nextMedia.localFileAvailable();
+ mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ isCasting ? EXTRA_CODE_CAST :
+ (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
+ } else {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ mediaPlayer.stop();
+ //stopSelf();
+ }
+ }
+ }
+
+ public void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) {
+ Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds");
+ taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ public void disableSleepTimer() {
+ taskManager.disableSleepTimer();
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
+ }
+
+ private void writePlaybackPreferencesNoMediaPlaying() {
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putInt(
+ PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS,
+ PlaybackPreferences.PLAYER_STATUS_OTHER);
+ editor.commit();
+ }
+
+ private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) {
+ int playerStatusAsInt;
+ switch (playerStatus) {
+ case PLAYING:
+ playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING;
+ break;
+ case PAUSED:
+ playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED;
+ break;
+ default:
+ playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER;
+ }
+ return playerStatusAsInt;
+ }
+
+ private void writePlaybackPreferences() {
+ Log.d(TAG, "Writing playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
+ MediaType mediaType = mediaPlayer.getCurrentMediaType();
+ boolean stream = mediaPlayer.isStreaming();
+ int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus);
+
+ if (info.playable != null) {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ info.playable.getPlayableType());
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
+ stream);
+ editor.putBoolean(
+ PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
+ mediaType == MediaType.VIDEO);
+ if (info.playable instanceof FeedMedia) {
+ FeedMedia fMedia = (FeedMedia) info.playable;
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ fMedia.getItem().getFeed().getId());
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ fMedia.getId());
+ } else {
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+ info.playable.writeToPreferences(editor);
+ } else {
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ editor.putLong(
+ PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
+ PlaybackPreferences.NO_MEDIA_PLAYING);
+ }
+ editor.putInt(
+ PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus);
+
+ editor.commit();
+ }
+
+ private void writePlayerStatusPlaybackPreferences() {
+ Log.d(TAG, "Writing player status playback preferences");
+
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()).edit();
+ int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus());
+
+ editor.putInt(
+ PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus);
+
+ editor.commit();
+ }
+
+ /**
+ * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute.
+ */
+ private void postStatusUpdateIntent() {
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ }
+
+ private void sendNotificationBroadcast(int type, int code) {
+ Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
+ intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
+ intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Updates the Media Session for the corresponding status.
+ * @param playerStatus the current {@link PlayerStatus}
+ */
+ private void updateMediaSession(final PlayerStatus playerStatus) {
+ PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder();
+
+ int state;
+ if (playerStatus != null) {
+ switch (playerStatus) {
+ case PLAYING:
+ state = PlaybackStateCompat.STATE_PLAYING;
+ break;
+ case PREPARED:
+ case PAUSED:
+ state = PlaybackStateCompat.STATE_PAUSED;
+ break;
+ case STOPPED:
+ state = PlaybackStateCompat.STATE_STOPPED;
+ break;
+ case SEEKING:
+ state = PlaybackStateCompat.STATE_FAST_FORWARDING;
+ break;
+ case PREPARING:
+ case INITIALIZING:
+ state = PlaybackStateCompat.STATE_CONNECTING;
+ break;
+ case INITIALIZED:
+ case INDETERMINATE:
+ state = PlaybackStateCompat.STATE_NONE;
+ break;
+ case ERROR:
+ state = PlaybackStateCompat.STATE_ERROR;
+ break;
+ default:
+ state = PlaybackStateCompat.STATE_NONE;
+ break;
+ }
+ } else {
+ state = PlaybackStateCompat.STATE_NONE;
+ }
+ sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed());
+ sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
+ mediaSession.setPlaybackState(sessionState.build());
+ }
+
+ /**
+ * Used by updateMediaSessionMetadata to load notification data in another thread.
+ */
+ private Thread mediaSessionSetupThread;
+
+ private void updateMediaSessionMetadata(final Playable p) {
+ if (p == null || mediaSession == null) {
+ return;
+ }
+ if (mediaSessionSetupThread != null) {
+ mediaSessionSetupThread.interrupt();
+ }
+
+ Runnable mediaSessionSetupTask = () -> {
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle());
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle());
+
+ if (p.getImageLocation() != null && UserPreferences.setLockscreenBackground()) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageLocation().toString());
+ try {
+ if (isCasting) {
+ Bitmap art = Glide.with(this)
+ .load(p.getImageLocation())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ } else {
+ WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Bitmap art = Glide.with(this)
+ .load(p.getImageLocation())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .centerCrop()
+ .into(display.getWidth(), display.getHeight())
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ }
+ } catch (Throwable tr) {
+ Log.e(TAG, Log.getStackTraceString(tr));
+ }
+ }
+ if (!Thread.currentThread().isInterrupted() && started) {
+ mediaSession.setMetadata(builder.build());
+ }
+ };
+
+ mediaSessionSetupThread = new Thread(mediaSessionSetupTask);
+ mediaSessionSetupThread.start();
+ }
+
+ /**
+ * Used by setupNotification to load notification data in another thread.
+ */
+ private Thread notificationSetupThread;
+
+ /**
+ * Prepares notification and starts the service in the foreground.
+ */
+ private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
+ final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
+ PlaybackService.getPlayerActivityIntent(this),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ if (notificationSetupThread != null) {
+ notificationSetupThread.interrupt();
+ }
+ Runnable notificationSetupTask = new Runnable() {
+ Bitmap icon = null;
+
+ @Override
+ public void run() {
+ Log.d(TAG, "Starting background work");
+ if (android.os.Build.VERSION.SDK_INT >= 11) {
+ if (info.playable != null) {
+ int iconSize = getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ try {
+ icon = Glide.with(PlaybackService.this)
+ .load(info.playable.getImageLocation())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .centerCrop()
+ .into(iconSize, iconSize)
+ .get();
+ } catch (Throwable tr) {
+ Log.e(TAG, "Error loading the media icon for the notification", tr);
+ }
+ }
+ }
+ if (icon == null) {
+ icon = BitmapFactory.decodeResource(getApplicationContext().getResources(),
+ ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()));
+ }
+
+ if (mediaPlayer == null) {
+ return;
+ }
+ PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
+ final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
+
+ if (!Thread.currentThread().isInterrupted() && started && info.playable != null) {
+ String contentText = info.playable.getEpisodeTitle();
+ String contentTitle = info.playable.getFeedTitle();
+ Notification notification;
+
+ // Builder is v7, even if some not overwritten methods return its parent's v4 interface
+ NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(
+ PlaybackService.this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setOngoing(false)
+ .setContentIntent(pIntent)
+ .setLargeIcon(icon)
+ .setSmallIcon(smallIcon)
+ .setWhen(0) // we don't need the time
+ .setPriority(UserPreferences.getNotifyPriority()); // set notification priority
+ IntList compactActionList = new IntList();
+
+ int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
+
+ if (isCasting) {
+ Intent stopCastingIntent = new Intent(PlaybackService.this, PlaybackService.class);
+ stopCastingIntent.putExtra(EXTRA_CAST_DISCONNECT, true);
+ PendingIntent stopCastingPendingIntent = PendingIntent.getService(PlaybackService.this,
+ numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ notificationBuilder.addAction(R.drawable.ic_media_cast_disconnect,
+ getString(R.string.cast_disconnect_label),
+ stopCastingPendingIntent);
+ numActions++;
+ }
+
+ // always let them rewind
+ PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_rew,
+ getString(R.string.rewind_label),
+ rewindButtonPendingIntent);
+ if(UserPreferences.showRewindOnCompactNotification()) {
+ compactActionList.add(numActions);
+ }
+ numActions++;
+
+ if (playerStatus == PlayerStatus.PLAYING) {
+ PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_PAUSE, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action
+ getString(R.string.pause_label),
+ pauseButtonPendingIntent);
+ compactActionList.add(numActions++);
+ } else {
+ PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action
+ getString(R.string.play_label),
+ playButtonPendingIntent);
+ compactActionList.add(numActions++);
+ }
+
+ // ff follows play, then we have skip (if it's present)
+ PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_ff,
+ getString(R.string.fast_forward_label),
+ ffButtonPendingIntent);
+ if(UserPreferences.showFastForwardOnCompactNotification()) {
+ compactActionList.add(numActions);
+ }
+ numActions++;
+
+ if (UserPreferences.isFollowQueue()) {
+ PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_NEXT, numActions);
+ notificationBuilder.addAction(android.R.drawable.ic_media_next,
+ getString(R.string.skip_episode_label),
+ skipButtonPendingIntent);
+ if(UserPreferences.showSkipOnCompactNotification()) {
+ compactActionList.add(numActions);
+ }
+ numActions++;
+ }
+
+ PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
+ KeyEvent.KEYCODE_MEDIA_STOP, numActions);
+ notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle()
+ .setMediaSession(mediaSession.getSessionToken())
+ .setShowActionsInCompactView(compactActionList.toArray())
+ .setShowCancelButton(true)
+ .setCancelButtonIntent(stopButtonPendingIntent))
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setColor(Notification.COLOR_DEFAULT);
+
+ notification = notificationBuilder.build();
+
+ if (playerStatus == PlayerStatus.PLAYING ||
+ playerStatus == PlayerStatus.PREPARING ||
+ playerStatus == PlayerStatus.SEEKING ||
+ isCasting) {
+ startForeground(NOTIFICATION_ID, notification);
+ } else {
+ stopForeground(false);
+ NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ Log.d(TAG, "Notification set up");
+ }
+ }
+ };
+ notificationSetupThread = new Thread(notificationSetupTask);
+ notificationSetupThread.start();
+ }
+
+ private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) {
+ Intent intent = new Intent(
+ PlaybackService.this, PlaybackService.class);
+ intent.putExtra(
+ MediaButtonReceiver.EXTRA_KEYCODE,
+ keycodeValue);
+ return PendingIntent
+ .getService(PlaybackService.this, requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /**
+ * Persists the current position and last played time of the media file.
+ *
+ * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects
+ * @param deltaPlayedDuration value by which played_duration should be increased.
+ */
+ private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) {
+ int position = getCurrentPosition();
+ int duration = getDuration();
+ float playbackSpeed = getCurrentPlaybackSpeed();
+ final Playable playable = mediaPlayer.getPlayable();
+ if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) {
+ Log.d(TAG, "Saving current position to " + position);
+ if (updatePlayedDuration && playable instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) playable;
+ FeedItem item = media.getItem();
+ media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed)));
+ // Auto flattr
+ if (isAutoFlattrable(media) &&
+ (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
+ Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration())
+ + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration));
+ DBTasks.flattrItemIfLoggedIn(this, item);
+ }
+ }
+ playable.saveCurrentPosition(
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext()),
+ position,
+ System.currentTimeMillis());
+ }
+ }
+
+ private void stopWidgetUpdater() {
+ taskManager.cancelWidgetUpdater();
+ sendBroadcast(new Intent(STOP_WIDGET_UPDATE));
+ }
+
+ private void updateWidget() {
+ PlaybackService.this.sendBroadcast(new Intent(
+ FORCE_WIDGET_UPDATE));
+ }
+
+ public boolean sleepTimerActive() {
+ return taskManager.isSleepTimerActive();
+ }
+
+ public long getSleepTimerTimeLeft() {
+ return taskManager.getSleepTimerTimeLeft();
+ }
+
+ private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) {
+ boolean isPlaying = false;
+
+ if (info.playerStatus == PlayerStatus.PLAYING) {
+ isPlaying = true;
+ }
+
+ if (info.playable != null) {
+ Intent i = new Intent(whatChanged);
+ i.putExtra("id", 1);
+ i.putExtra("artist", "");
+ i.putExtra("album", info.playable.getFeedTitle());
+ i.putExtra("track", info.playable.getEpisodeTitle());
+ i.putExtra("playing", isPlaying);
+ final List<FeedItem> queue = taskManager.getQueueIfLoaded();
+ if (queue != null) {
+ i.putExtra("ListSize", queue.size());
+ }
+ i.putExtra("duration", info.playable.getDuration());
+ i.putExtra("position", info.playable.getPosition());
+ sendBroadcast(i);
+ }
+ }
+
+ /**
+ * Pauses playback when the headset is disconnected and the preference is
+ * set
+ */
+ private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private static final String TAG = "headsetDisconnected";
+ private static final int UNPLUGGED = 0;
+ private static final int PLUGGED = 1;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) {
+ int state = intent.getIntExtra("state", -1);
+ if (state != -1) {
+ Log.d(TAG, "Headset plug event. State is " + state);
+ if (state == UNPLUGGED) {
+ Log.d(TAG, "Headset was unplugged during playback.");
+ pauseIfPauseOnDisconnect();
+ } else if (state == PLUGGED) {
+ Log.d(TAG, "Headset was plugged in during playback.");
+ unpauseIfPauseOnDisconnect(false);
+ }
+ } else {
+ Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
+ int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
+ if (state == BluetoothA2dp.STATE_CONNECTED) {
+ Log.d(TAG, "Received bluetooth connection intent");
+ unpauseIfPauseOnDisconnect(true);
+ }
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // sound is about to change, eg. bluetooth -> speaker
+ Log.d(TAG, "Pausing playback because audio is becoming noisy");
+ pauseIfPauseOnDisconnect();
+ }
+ // android.media.AUDIO_BECOMING_NOISY
+ };
+
+ /**
+ * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
+ */
+ private void pauseIfPauseOnDisconnect() {
+ if (UserPreferences.isPauseOnHeadsetDisconnect()) {
+ if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
+ transientPause = true;
+ }
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
+ }
+ }
+
+ /**
+ * @param bluetooth true if the event for unpausing came from bluetooth
+ */
+ private void unpauseIfPauseOnDisconnect(boolean bluetooth) {
+ if (transientPause) {
+ transientPause = false;
+ if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) {
+ mediaPlayer.resume();
+ } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()){
+ // let the user know we've started playback again...
+ Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
+ if(v != null) {
+ v.vibrate(500);
+ }
+ mediaPlayer.resume();
+ }
+ }
+ }
+
+ private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
+ stopSelf();
+ }
+ }
+
+ };
+
+ private final BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
+ Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
+ mediaPlayer.endPlayback(true, false);
+ }
+ }
+ };
+
+ private final BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) {
+ Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent");
+ mediaPlayer.resume();
+ }
+ }
+ };
+
+ private final BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (TextUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) {
+ Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent");
+ mediaPlayer.pause(false, false);
+ }
+ }
+ };
+
+ public static MediaType getCurrentMediaType() {
+ return currentMediaType;
+ }
+
+ public static boolean isCasting() {
+ return isCasting;
+ }
+
+ public void resume() {
+ mediaPlayer.resume();
+ }
+
+ public void prepare() {
+ mediaPlayer.prepare();
+ }
+
+ public void pause(boolean abandonAudioFocus, boolean reinit) {
+ mediaPlayer.pause(abandonAudioFocus, reinit);
+ }
+
+ public void reinit() {
+ mediaPlayer.reinit();
+ }
+
+ public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() {
+ return mediaPlayer.getPSMPInfo();
+ }
+
+ public PlayerStatus getStatus() {
+ return mediaPlayer.getPlayerStatus();
+ }
+
+ public Playable getPlayable() { return mediaPlayer.getPlayable(); }
+
+ public boolean canSetSpeed() {
+ return mediaPlayer.canSetSpeed();
+ }
+
+ public void setSpeed(float speed) {
+ mediaPlayer.setSpeed(speed);
+ }
+
+ public void setVolume(float leftVolume, float rightVolume) {
+ mediaPlayer.setVolume(leftVolume, rightVolume);
+ }
+
+ public float getCurrentPlaybackSpeed() {
+ return mediaPlayer.getPlaybackSpeed();
+ }
+
+ public boolean canDownmix() {
+ return mediaPlayer.canDownmix();
+ }
+
+ public void setDownmix(boolean enable) {
+ mediaPlayer.setDownmix(enable);
+ }
+
+ public boolean isStartWhenPrepared() {
+ return mediaPlayer.isStartWhenPrepared();
+ }
+
+ public void setStartWhenPrepared(boolean s) {
+ mediaPlayer.setStartWhenPrepared(s);
+ }
+
+
+ public void seekTo(final int t) {
+ if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING
+ && GpodnetPreferences.loggedIn()) {
+ final Playable playable = mediaPlayer.getPlayable();
+ if (playable instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) playable;
+ FeedItem item = media.getItem();
+ GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
+ .currentDeviceId()
+ .currentTimestamp()
+ .started(startPosition / 1000)
+ .position(getCurrentPosition() / 1000)
+ .total(getDuration() / 1000)
+ .build();
+ GpodnetPreferences.enqueueEpisodeAction(action);
+ }
+ }
+ mediaPlayer.seekTo(t);
+ if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) {
+ startPosition = t;
+ }
+ }
+
+
+ public void seekDelta(final int d) {
+ mediaPlayer.seekDelta(d);
+ }
+
+ /**
+ * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter)
+ */
+ public void seekToChapter(Chapter c) {
+ mediaPlayer.seekToChapter(c);
+ }
+
+ /**
+ * call getDuration() on mediaplayer or return INVALID_TIME if player is in
+ * an invalid state.
+ */
+ public int getDuration() {
+ return mediaPlayer.getDuration();
+ }
+
+ /**
+ * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player
+ * is in an invalid state.
+ */
+ public int getCurrentPosition() {
+ return mediaPlayer.getPosition();
+ }
+
+ public boolean isStreaming() {
+ return mediaPlayer.isStreaming();
+ }
+
+ public Pair<Integer, Integer> getVideoSize() {
+ return mediaPlayer.getVideoSize();
+ }
+
+ private boolean isAutoFlattrable(FeedMedia media) {
+ if (media != null) {
+ FeedItem item = media.getItem();
+ return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred();
+ } else {
+ return false;
+ }
+ }
+
+// private CastConsumer castConsumer = new DefaultCastConsumer() {
+// @Override
+// public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
+// PlaybackService.this.onCastAppConnected(wasLaunched);
+// }
+//
+// @Override
+// public void onDisconnectionReason(int reason) {
+// Log.d(TAG, "onDisconnectionReason() with code " + reason);
+// // This is our final chance to update the underlying stream position
+// // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer
+// // is disconnected and hence we update our local value of stream position
+// // to the latest position.
+// if (mediaPlayer != null) {
+// saveCurrentPosition(false, 0);
+// infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo();
+// if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT &&
+// infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) {
+// // If it's NOT based on user action, we shouldn't automatically resume local playback
+// infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED;
+// }
+// }
+// }
+//
+// @Override
+// public void onDisconnected() {
+// Log.d(TAG, "onDisconnected()");
+// isCasting = false;
+// PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection;
+// infoBeforeCastDisconnection = null;
+// if (info == null && mediaPlayer != null) {
+// info = mediaPlayer.getPSMPInfo();
+// }
+// if (info == null) {
+// info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null);
+// }
+// switchMediaPlayer(new LocalPSMP(PlaybackService.this, mediaPlayerCallback),
+// info, true);
+// if (info.playable != null) {
+// sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+// info.playable.getMediaType() == MediaType.AUDIO ? EXTRA_CODE_AUDIO : EXTRA_CODE_VIDEO);
+// } else {
+// Log.d(TAG, "Cast session disconnected, but no current media");
+// sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+// }
+// // hardware volume buttons control the local device volume
+// mediaRouter.setMediaSessionCompat(null);
+// unregisterWifiBroadcastReceiver();
+// PlayerStatus status = info.playerStatus;
+// if ((status == PlayerStatus.PLAYING ||
+// status == PlayerStatus.SEEKING ||
+// status == PlayerStatus.PREPARING ||
+// UserPreferences.isPersistNotify()) &&
+// android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+// setupNotification(info);
+// } else if (!UserPreferences.isPersistNotify()){
+// stopForeground(true);
+// }
+// }
+// };
+
+ private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() {
+
+ private static final String TAG = "MediaSessionCompat";
+
+ @Override
+ public void onPlay() {
+ Log.d(TAG, "onPlay()");
+ PlayerStatus status = getStatus();
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
+ resume();
+ } else if (status == PlayerStatus.INITIALIZED) {
+ setStartWhenPrepared(true);
+ prepare();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ Log.d(TAG, "onPause()");
+ if (getStatus() == PlayerStatus.PLAYING) {
+ pause(false, true);
+ }
+ if (UserPreferences.isPersistNotify()) {
+ pause(false, true);
+ } else {
+ pause(true, true);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ Log.d(TAG, "onStop()");
+ mediaPlayer.stop();
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ Log.d(TAG, "onSkipToPrevious()");
+ seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ }
+
+ @Override
+ public void onRewind() {
+ Log.d(TAG, "onRewind()");
+ seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ }
+
+ @Override
+ public void onFastForward() {
+ Log.d(TAG, "onFastForward()");
+ seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ Log.d(TAG, "onSkipToNext()");
+ if(UserPreferences.shouldHardwareButtonSkip()) {
+ mediaPlayer.endPlayback(true, false);
+ } else {
+ seekDelta(UserPreferences.getFastFowardSecs() * 1000);
+ }
+ }
+
+
+ @Override
+ public void onSeekTo(long pos) {
+ Log.d(TAG, "onSeekTo()");
+ seekTo((int) pos);
+ }
+
+ @Override
+ public boolean onMediaButtonEvent(final Intent mediaButton) {
+ Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")");
+ if (mediaButton != null) {
+ KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ if (keyEvent != null &&
+ keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
+ keyEvent.getRepeatCount() == 0){
+ handleKeycode(keyEvent.getKeyCode(), keyEvent.getSource());
+ }
+ }
+ return false;
+ }
+ };
+
+// private void onCastAppConnected(boolean wasLaunched) {
+// Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined"));
+// isCasting = true;
+// PlaybackServiceMediaPlayer.PSMPInfo info = null;
+// if (mediaPlayer != null) {
+// info = mediaPlayer.getPSMPInfo();
+// if (info.playerStatus == PlayerStatus.PLAYING) {
+// // could be pause, but this way we make sure the new player will get the correct position,
+// // since pause runs asynchronously and we could be directing the new player to play even before
+// // the old player gives us back the position.
+// saveCurrentPosition(false, 0);
+// }
+// }
+// if (info == null) {
+// info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null);
+// }
+// sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, EXTRA_CODE_CAST);
+// switchMediaPlayer(new RemotePSMP(PlaybackService.this, mediaPlayerCallback),
+// info,
+// wasLaunched);
+// // hardware volume buttons control the remote device volume
+// mediaRouter.setMediaSessionCompat(mediaSession);
+// registerWifiBroadcastReceiver();
+// setupNotification(info);
+// }
+
+ private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,
+ @NonNull PlaybackServiceMediaPlayer.PSMPInfo info,
+ boolean wasLaunched) {
+ if (mediaPlayer != null) {
+ mediaPlayer.endPlayback(true, true);
+ mediaPlayer.shutdownQuietly();
+ }
+ mediaPlayer = newPlayer;
+ Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName());
+ if (!wasLaunched) {
+ PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo();
+ if (candidate.playable != null &&
+ candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) {
+ // do not automatically send new media to cast device
+ info.playable = null;
+ }
+ }
+ if (info.playable != null) {
+ mediaPlayer.playMediaObject(info.playable,
+ !info.playable.localFileAvailable(),
+ info.playerStatus == PlayerStatus.PLAYING,
+ info.playerStatus.isAtLeast(PlayerStatus.PREPARING));
+ }
+ }
+
+ private void registerWifiBroadcastReceiver() {
+ if (wifiBroadcastReceiver != null) {
+ return;
+ }
+ wifiBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+ NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+ boolean isConnected = info.isConnected();
+ //apparently this method gets called twice when a change happens, but one run is enough.
+ if (isConnected && !wifiConnectivity) {
+ wifiConnectivity = true;
+// castManager.startCastDiscovery();
+// castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid());
+ } else {
+ wifiConnectivity = isConnected;
+ }
+ }
+ }
+ };
+ registerReceiver(wifiBroadcastReceiver,
+ new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
+ }
+
+ private void unregisterWifiBroadcastReceiver() {
+ if (wifiBroadcastReceiver != null) {
+ unregisterReceiver(wifiBroadcastReceiver);
+ wifiBroadcastReceiver = null;
+ }
+ }
+
+ private SharedPreferences.OnSharedPreferenceChangeListener prefListener =
+ (sharedPreferences, key) -> {
+// if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
+// if (!UserPreferences.isCastEnabled()) {
+// if (castManager.isConnecting() || castManager.isConnected()) {
+// Log.d(TAG, "Disconnecting cast device due to a change in user preferences");
+// castManager.disconnect();
+// }
+// }
+// } else
+ if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
+ updateMediaSessionMetadata(getPlayable());
+ }
+ };
+}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
new file mode 100644
index 000000000..abf787ce8
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
@@ -0,0 +1,592 @@
+//package de.danoeh.antennapod.core.service.playback;
+//
+//import android.content.Context;
+//import android.media.MediaPlayer;
+//import android.support.annotation.NonNull;
+//import android.util.Log;
+//import android.util.Pair;
+//import android.view.SurfaceHolder;
+//
+//import com.google.android.gms.cast.Cast;
+//import com.google.android.gms.cast.CastStatusCodes;
+//import com.google.android.gms.cast.MediaInfo;
+//import com.google.android.gms.cast.MediaStatus;
+//import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
+//import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
+//import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
+//
+//import java.util.concurrent.atomic.AtomicBoolean;
+//
+//import de.danoeh.antennapod.core.R;
+//import de.danoeh.antennapod.core.cast.CastConsumer;
+//import de.danoeh.antennapod.core.cast.CastManager;
+//import de.danoeh.antennapod.core.cast.CastUtils;
+//import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
+//import de.danoeh.antennapod.core.cast.RemoteMedia;
+//import de.danoeh.antennapod.core.feed.FeedMedia;
+//import de.danoeh.antennapod.core.feed.MediaType;
+//import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+//import de.danoeh.antennapod.core.util.playback.Playable;
+//
+///**
+// * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
+// */
+//public class RemotePSMP extends PlaybackServiceMediaPlayer {
+//
+// public static final String TAG = "RemotePSMP";
+//
+// public static final int CAST_ERROR = 3001;
+//
+// public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
+//
+// private final CastManager castMgr;
+//
+// private volatile Playable media;
+// private volatile MediaInfo remoteMedia;
+// private volatile MediaType mediaType;
+//
+// private final AtomicBoolean isBuffering;
+//
+// private final AtomicBoolean startWhenPrepared;
+//
+// public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
+// super(context, callback);
+//
+// castMgr = CastManager.getInstance();
+// media = null;
+// mediaType = null;
+// startWhenPrepared = new AtomicBoolean(false);
+// isBuffering = new AtomicBoolean(false);
+//
+// try {
+// if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
+// // updates the state, but does not start playing new media if it was going to
+// onRemoteMediaPlayerStatusUpdated(
+// ((p, playNextEpisode, wasSkipped, switchingPlayers) ->
+// this.callback.endPlayback(p, false, wasSkipped, switchingPlayers)));
+// }
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to do initial check for loaded media", e);
+// }
+//
+// castMgr.addCastConsumer(castConsumer);
+// //TODO
+// }
+//
+// private CastConsumer castConsumer = new DefaultCastConsumer() {
+// @Override
+// public void onRemoteMediaPlayerMetadataUpdated() {
+// RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback);
+// }
+//
+// @Override
+// public void onRemoteMediaPlayerStatusUpdated() {
+// RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback);
+// }
+//
+// @Override
+// public void onMediaLoadResult(int statusCode) {
+// if (playerStatus == PlayerStatus.PREPARING) {
+// if (statusCode == CastStatusCodes.SUCCESS) {
+// setPlayerStatus(PlayerStatus.PREPARED, media);
+// if (media.getDuration() == 0) {
+// Log.d(TAG, "Setting duration of media");
+// try {
+// media.setDuration((int) castMgr.getMediaDuration());
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to get remote media's duration");
+// }
+// }
+// } else if (statusCode != CastStatusCodes.REPLACED){
+// Log.d(TAG, "Remote media failed to load");
+// setPlayerStatus(PlayerStatus.INITIALIZED, media);
+// }
+// } else {
+// Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result");
+// }
+// }
+//
+// @Override
+// public void onApplicationStatusChanged(String appStatus) {
+// if (playerStatus != PlayerStatus.PLAYING) {
+// Log.d(TAG, "onApplicationStatusChanged, but no media was playing");
+// return;
+// }
+// boolean playbackEnded = false;
+// try {
+// int standbyState = castMgr.getApplicationStandbyState();
+// Log.d(TAG, "standbyState: " + standbyState);
+// playbackEnded = standbyState == Cast.STANDBY_STATE_YES;
+// } catch (IllegalStateException e) {
+// Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()");
+// }
+// if (playbackEnded) {
+// setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+// callback.endPlayback(media, true, false, false);
+// }
+// }
+//
+// @Override
+// public void onFailed(int resourceId, int statusCode) {
+// callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
+// }
+// };
+//
+// private void setBuffering(boolean buffering) {
+// if (buffering && isBuffering.compareAndSet(false, true)) {
+// callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
+// } else if (!buffering && isBuffering.compareAndSet(true, false)) {
+// callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
+// }
+// }
+//
+// private Playable localVersion(MediaInfo info){
+// if (info == null) {
+// return null;
+// }
+// if (CastUtils.matches(info, media)) {
+// return media;
+// }
+// return CastUtils.getPlayable(info, true);
+// }
+//
+// private MediaInfo remoteVersion(Playable playable) {
+// if (playable == null) {
+// return null;
+// }
+// if (CastUtils.matches(remoteMedia, playable)) {
+// return remoteMedia;
+// }
+// if (playable instanceof FeedMedia) {
+// return CastUtils.convertFromFeedMedia((FeedMedia) playable);
+// }
+// if (playable instanceof RemoteMedia) {
+// return ((RemoteMedia) playable).extractMediaInfo();
+// }
+// return null;
+// }
+//
+// private void onRemoteMediaPlayerStatusUpdated(@NonNull EndPlaybackCall endPlaybackCall) {
+// MediaStatus status = castMgr.getMediaStatus();
+// if (status == null) {
+// Log.d(TAG, "Received null MediaStatus");
+// //setBuffering(false);
+// //setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+// return;
+// } else {
+// Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState());
+// }
+// Playable currentMedia = localVersion(status.getMediaInfo());
+// boolean updateUI = currentMedia != media;
+// if (currentMedia != null) {
+// long position = status.getStreamPosition();
+// if (position > 0 && currentMedia.getPosition() == 0) {
+// currentMedia.setPosition((int) position);
+// }
+// }
+// int state = status.getPlayerState();
+// setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING);
+// switch (state) {
+// case MediaStatus.PLAYER_STATE_PLAYING:
+// setPlayerStatus(PlayerStatus.PLAYING, currentMedia);
+// break;
+// case MediaStatus.PLAYER_STATE_PAUSED:
+// setPlayerStatus(PlayerStatus.PAUSED, currentMedia);
+// break;
+// case MediaStatus.PLAYER_STATE_BUFFERING:
+// setPlayerStatus(playerStatus, currentMedia);
+// break;
+// case MediaStatus.PLAYER_STATE_IDLE:
+// int reason = status.getIdleReason();
+// switch (reason) {
+// case MediaStatus.IDLE_REASON_CANCELED:
+// // check if we're already loading something else
+// if (!updateUI || media == null) {
+// setPlayerStatus(PlayerStatus.STOPPED, currentMedia);
+// } else {
+// updateUI = false;
+// }
+// break;
+// case MediaStatus.IDLE_REASON_INTERRUPTED:
+// // check if we're already loading something else
+// if (!updateUI || media == null) {
+// setPlayerStatus(PlayerStatus.PREPARING, currentMedia);
+// } else {
+// updateUI = false;
+// }
+// break;
+// case MediaStatus.IDLE_REASON_NONE:
+// setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia);
+// break;
+// case MediaStatus.IDLE_REASON_FINISHED:
+// boolean playing = playerStatus == PlayerStatus.PLAYING;
+// setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+// endPlaybackCall.endPlayback(currentMedia,playing, false, false);
+// // endPlayback already updates the UI, so no need to trigger it ourselves
+// updateUI = false;
+// break;
+// case MediaStatus.IDLE_REASON_ERROR:
+// Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
+// setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+// callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
+// R.string.cast_failed_media_error_skipping);
+// endPlaybackCall.endPlayback(currentMedia, startWhenPrepared.get(), true, false);
+// // endPlayback already updates the UI, so no need to trigger it ourselves
+// updateUI = false;
+// }
+// break;
+// case MediaStatus.PLAYER_STATE_UNKNOWN:
+// //is this right?
+// setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+// break;
+// default:
+// Log.e(TAG, "Remote media state undetermined!");
+// setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+// }
+// if (updateUI) {
+// callback.onMediaChanged(true);
+// }
+// }
+//
+// @Override
+// public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+// Log.d(TAG, "playMediaObject() called");
+// playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+// }
+//
+// /**
+// * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+// * the given playable parameter is the same object as the currently playing media.
+// *
+// * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
+// */
+// private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+// if (!CastUtils.isCastable(playable)) {
+// Log.d(TAG, "media provided is not compatible with cast device");
+// callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
+// try {
+// playable.loadMetadata();
+// } catch (Playable.PlayableException e) {
+// Log.e(TAG, "Unable to load metadata of playable", e);
+// }
+// callback.endPlayback(playable, startWhenPrepared, true, false);
+// return;
+// }
+//
+// if (media != null) {
+// if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
+// && playerStatus == PlayerStatus.PLAYING) {
+// // episode is already playing -> ignore method call
+// Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
+// return;
+// } else {
+// // set temporarily to pause in order to update list with current position
+// try {
+// if (castMgr.isRemoteMediaPlaying()) {
+// setPlayerStatus(PlayerStatus.PAUSED, media);
+// }
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
+// // this might end up just being pointless if we need to query the remote device for the position
+// if (playerStatus == PlayerStatus.PLAYING) {
+// setPlayerStatus(PlayerStatus.PAUSED, media);
+// }
+// }
+// smartMarkAsPlayed(media);
+//
+//
+// setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+// }
+// }
+//
+// this.media = playable;
+// remoteMedia = remoteVersion(playable);
+// //this.stream = stream;
+// this.mediaType = media.getMediaType();
+// this.startWhenPrepared.set(startWhenPrepared);
+// setPlayerStatus(PlayerStatus.INITIALIZING, media);
+// try {
+// media.loadMetadata();
+// callback.onMediaChanged(true);
+// setPlayerStatus(PlayerStatus.INITIALIZED, media);
+// if (prepareImmediately) {
+// prepare();
+// }
+// } catch (Playable.PlayableException e) {
+// Log.e(TAG, "Error while loading media metadata", e);
+// setPlayerStatus(PlayerStatus.STOPPED, null);
+// }
+// }
+//
+// @Override
+// public void resume() {
+// try {
+// // TODO see comment on prepare()
+// // setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+// if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+// int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+// media.getPosition(),
+// media.getLastPlayedTime());
+// castMgr.play(newPosition);
+// }
+// castMgr.play();
+// } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to resume remote playback", e);
+// }
+// }
+//
+// @Override
+// public void pause(boolean abandonFocus, boolean reinit) {
+// try {
+// if (castMgr.isRemoteMediaPlaying()) {
+// castMgr.pause();
+// }
+// } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to pause", e);
+// }
+// }
+//
+// @Override
+// public void prepare() {
+// if (playerStatus == PlayerStatus.INITIALIZED) {
+// Log.d(TAG, "Preparing media player");
+// setPlayerStatus(PlayerStatus.PREPARING, media);
+// try {
+// int position = media.getPosition();
+// if (position > 0) {
+// position = RewindAfterPauseUtils.calculatePositionWithRewind(
+// position,
+// media.getLastPlayedTime());
+// }
+// // TODO We're not supporting user set stream volume yet, as we need to make a UI
+// // that doesn't allow changing playback speed or have different values for left/right
+// //setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+// castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Error loading media", e);
+// setPlayerStatus(PlayerStatus.INITIALIZED, media);
+// }
+// }
+// }
+//
+// @Override
+// public void reinit() {
+// Log.d(TAG, "reinit() called");
+// if (media != null) {
+// playMediaObject(media, true, false, startWhenPrepared.get(), false);
+// } else {
+// Log.d(TAG, "Call to reinit was ignored: media was null");
+// }
+// }
+//
+// @Override
+// public void seekTo(int t) {
+// //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
+// try {
+// if (castMgr.isRemoteMediaLoaded()) {
+// setPlayerStatus(PlayerStatus.SEEKING, media);
+// castMgr.seek(t);
+// } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
+// media.setPosition(t);
+// startWhenPrepared.set(false);
+// prepare();
+// }
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to seek", e);
+// }
+// }
+//
+// @Override
+// public void seekDelta(int d) {
+// int position = getPosition();
+// if (position != INVALID_TIME) {
+// seekTo(position + d);
+// } else {
+// Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+// }
+// }
+//
+// @Override
+// public int getDuration() {
+// int retVal = INVALID_TIME;
+// boolean prepared;
+// try {
+// prepared = castMgr.isRemoteMediaLoaded();
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to check if remote media is loaded", e);
+// prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
+// }
+// if (prepared) {
+// try {
+// retVal = (int) castMgr.getMediaDuration();
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to determine remote media's duration", e);
+// }
+// }
+// if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
+// retVal = media.getDuration();
+// }
+// Log.d(TAG, "getDuration() -> " + retVal);
+// return retVal;
+// }
+//
+// @Override
+// public int getPosition() {
+// int retVal = INVALID_TIME;
+// boolean prepared;
+// try {
+// prepared = castMgr.isRemoteMediaLoaded();
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to check if remote media is loaded", e);
+// prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
+// }
+// if (prepared) {
+// try {
+// retVal = (int) castMgr.getCurrentMediaPosition();
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Unable to determine remote media's position", e);
+// }
+// }
+// if(retVal <= 0 && media != null && media.getPosition() >= 0) {
+// retVal = media.getPosition();
+// }
+// Log.d(TAG, "getPosition() -> " + retVal);
+// return retVal;
+// }
+//
+// @Override
+// public boolean isStartWhenPrepared() {
+// return startWhenPrepared.get();
+// }
+//
+// @Override
+// public void setStartWhenPrepared(boolean startWhenPrepared) {
+// this.startWhenPrepared.set(startWhenPrepared);
+// }
+//
+// //TODO I believe some parts of the code make the same decision skipping this check, so that
+// //should be changed as well
+// @Override
+// public boolean canSetSpeed() {
+// return false;
+// }
+//
+// @Override
+// public void setSpeed(float speed) {
+// throw new UnsupportedOperationException("Setting playback speed unsupported for Remote Playback");
+// }
+//
+// @Override
+// public float getPlaybackSpeed() {
+// return 1;
+// }
+//
+// @Override
+// public void setVolume(float volumeLeft, float volumeRight) {
+// Log.d(TAG, "Setting the Stream volume on Remote Media Player");
+// double volume = (volumeLeft+volumeRight)/2;
+// if (volume > 1.0) {
+// volume = 1.0;
+// }
+// if (volume < 0.0) {
+// volume = 0.0;
+// }
+// try {
+// castMgr.setStreamVolume(volume);
+// } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
+// Log.e(TAG, "Unable to set the volume", e);
+// }
+// }
+//
+// @Override
+// public boolean canDownmix() {
+// return false;
+// }
+//
+// @Override
+// public void setDownmix(boolean enable) {
+// throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player");
+// }
+//
+// @Override
+// public MediaType getCurrentMediaType() {
+// return mediaType;
+// }
+//
+// @Override
+// public boolean isStreaming() {
+// return true;
+// }
+//
+// @Override
+// public void shutdown() {
+// castMgr.removeCastConsumer(castConsumer);
+// }
+//
+// @Override
+// public void shutdownQuietly() {
+// shutdown();
+// }
+//
+// @Override
+// public void setVideoSurface(SurfaceHolder surface) {
+// throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
+// }
+//
+// @Override
+// public void resetVideoSurface() {
+// Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player");
+// }
+//
+// @Override
+// public Pair<Integer, Integer> getVideoSize() {
+// return null;
+// }
+//
+// @Override
+// public Playable getPlayable() {
+// return media;
+// }
+//
+// @Override
+// protected void setPlayable(Playable playable) {
+// if (playable != media) {
+// media = playable;
+// remoteMedia = remoteVersion(playable);
+// }
+// }
+//
+// @Override
+// public void endPlayback(boolean wasSkipped, boolean switchingPlayers) {
+// Log.d(TAG, "endPlayback() called");
+// boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
+// try {
+// isPlaying = castMgr.isRemoteMediaPlaying();
+// } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+// Log.e(TAG, "Could not determine if media is playing", e);
+// }
+// // TODO make sure we stop playback whenever there's no next episode.
+// if (playerStatus != PlayerStatus.INDETERMINATE) {
+// setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+// }
+// callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers);
+// }
+//
+// @Override
+// public void stop() {
+// if (playerStatus == PlayerStatus.INDETERMINATE) {
+// setPlayerStatus(PlayerStatus.STOPPED, null);
+// } else {
+// Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
+// }
+// }
+//
+// @Override
+// protected boolean shouldLockWifi() {
+// return false;
+// }
+//
+// private interface EndPlaybackCall {
+// boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers);
+// }
+//}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/free/java/de/danoeh/antennapod/core/util/playback/Playable.java
new file mode 100644
index 000000000..bc22e063c
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -0,0 +1,245 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.List;
+
+import de.danoeh.antennapod.core.asynctask.ImageResource;
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.ShownotesProvider;
+
+/**
+ * Interface for objects that can be played by the PlaybackService.
+ */
+public interface Playable extends Parcelable,
+ ShownotesProvider, ImageResource {
+
+ /**
+ * Save information about the playable in a preference so that it can be
+ * restored later via PlayableUtils.createInstanceFromPreferences.
+ * Implementations must NOT call commit() after they have written the values
+ * to the preferences file.
+ */
+ void writeToPreferences(SharedPreferences.Editor prefEditor);
+
+ /**
+ * This method is called from a separate thread by the PlaybackService.
+ * Playable objects should load their metadata in this method. This method
+ * should execute as quickly as possible and NOT load chapter marks if no
+ * local file is available.
+ */
+ void loadMetadata() throws PlayableException;
+
+ /**
+ * This method is called from a separate thread by the PlaybackService.
+ * Playable objects should load their chapter marks in this method if no
+ * local file was available when loadMetadata() was called.
+ */
+ void loadChapterMarks();
+
+ /**
+ * Returns the title of the episode that this playable represents
+ */
+ String getEpisodeTitle();
+
+ /**
+ * Returns a list of chapter marks or null if this Playable has no chapters.
+ */
+ List<Chapter> getChapters();
+
+ /**
+ * Returns a link to a website that is meant to be shown in a browser
+ */
+ String getWebsiteLink();
+
+ String getPaymentLink();
+
+ /**
+ * Returns the title of the feed this Playable belongs to.
+ */
+ String getFeedTitle();
+
+ /**
+ * Returns a unique identifier, for example a file url or an ID from a
+ * database.
+ */
+ Object getIdentifier();
+
+ /**
+ * Return duration of object or 0 if duration is unknown.
+ */
+ int getDuration();
+
+ /**
+ * Return position of object or 0 if position is unknown.
+ */
+ int getPosition();
+
+ /**
+ * Returns last time (in ms) when this playable was played or 0
+ * if last played time is unknown.
+ */
+ long getLastPlayedTime();
+
+ /**
+ * Returns the type of media. This method should return the correct value
+ * BEFORE loadMetadata() is called.
+ */
+ MediaType getMediaType();
+
+ /**
+ * Returns an url to a local file that can be played or null if this file
+ * does not exist.
+ */
+ String getLocalMediaUrl();
+
+ /**
+ * Returns an url to a file that can be streamed by the player or null if
+ * this url is not known.
+ */
+ String getStreamUrl();
+
+ /**
+ * Returns true if a local file that can be played is available. getFileUrl
+ * MUST return a non-null string if this method returns true.
+ */
+ boolean localFileAvailable();
+
+ /**
+ * Returns true if a streamable file is available. getStreamUrl MUST return
+ * a non-null string if this method returns true.
+ */
+ boolean streamAvailable();
+
+ /**
+ * Saves the current position of this object. Implementations can use the
+ * provided SharedPreference to save this information and retrieve it later
+ * via PlayableUtils.createInstanceFromPreferences.
+ *
+ * @param pref shared prefs that might be used to store this object
+ * @param newPosition new playback position in ms
+ * @param timestamp current time in ms
+ */
+ void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp);
+
+ void setPosition(int newPosition);
+
+ void setDuration(int newDuration);
+
+ /**
+ * @param lastPlayedTimestamp timestamp in ms
+ */
+ void setLastPlayedTime(long lastPlayedTimestamp);
+
+ /**
+ * Is called by the PlaybackService when playback starts.
+ */
+ void onPlaybackStart();
+
+ /**
+ * Is called by the PlaybackService when playback is completed.
+ */
+ void onPlaybackCompleted();
+
+ /**
+ * Returns an integer that must be unique among all Playable classes. The
+ * return value is later used by PlayableUtils to determine the type of the
+ * Playable object that is restored.
+ */
+ int getPlayableType();
+
+ void setChapters(List<Chapter> chapters);
+
+ /**
+ * Provides utility methods for Playable objects.
+ */
+ class PlayableUtils {
+ private static final String TAG = "PlayableUtils";
+
+ /**
+ * Restores a playable object from a sharedPreferences file. This method might load data from the database,
+ * depending on the type of playable that was restored.
+ *
+ * @param type An integer that represents the type of the Playable object
+ * that is restored.
+ * @param pref The SharedPreferences file from which the Playable object
+ * is restored
+ * @return The restored Playable object
+ */
+ public static Playable createInstanceFromPreferences(Context context, int type,
+ SharedPreferences pref) {
+ Playable result = null;
+ // ADD new Playable types here:
+ switch (type) {
+ case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
+ result = createFeedMediaInstance(pref);
+ break;
+ case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
+ result = createExternalMediaInstance(pref);
+ break;
+// case RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA:
+// result = createRemoteMediaInstance(pref);
+// break;
+ }
+ if (result == null) {
+ Log.e(TAG, "Could not restore Playable object from preferences");
+ }
+ return result;
+ }
+
+ private static Playable createFeedMediaInstance(SharedPreferences pref) {
+ Playable result = null;
+ long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
+ if (mediaId != -1) {
+ result = DBReader.getFeedMedia(mediaId);
+ }
+ return result;
+ }
+
+ private static Playable createExternalMediaInstance(SharedPreferences pref) {
+ Playable result = null;
+ String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null);
+ String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null);
+ if (source != null && mediaType != null) {
+ int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
+ long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0);
+ result = new ExternalMedia(source, MediaType.valueOf(mediaType),
+ position, lastPlayedTime);
+ }
+ return result;
+ }
+
+ private static Playable createRemoteMediaInstance(SharedPreferences pref) {
+ //TODO there's probably no point in restoring RemoteMedia from preferences, because we
+ //only care about it while it's playing on the cast device.
+ return null;
+ }
+ }
+
+ class PlayableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PlayableException() {
+ super();
+ }
+
+ public PlayableException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public PlayableException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public PlayableException(Throwable throwable) {
+ super(throwable);
+ }
+
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
index fdde4b34c..9d8f4adf8 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.LongList;
public class FeedItemFilter {
private final String[] mProperties;
@@ -66,13 +67,14 @@ public class FeedItemFilter {
if (showQueued && showNotQueued) return result;
if (showDownloaded && showNotDownloaded) return result;
+ final LongList queuedIds = DBReader.getQueueIDList();
for(FeedItem item : items) {
// If the item does not meet a requirement, skip it.
if (showPlayed && !item.isPlayed()) continue;
if (showUnplayed && item.isPlayed()) continue;
if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue;
- boolean queued = DBReader.getQueueIDList().contains(item.getId());
+ boolean queued = queuedIds.contains(item.getId());
if (showQueued && !queued) continue;
if (showNotQueued && queued) continue;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
index 556c008d4..94a722da9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
@@ -185,8 +185,7 @@ public class HttpDownloader extends Downloader {
if(request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
String contentType = response.header("Content-Type");
Log.d(TAG, "content type: " + contentType);
- if(!contentType.startsWith("audio/") && !contentType.startsWith("video/") &&
- !contentType.equals("application/octet-stream")) {
+ if(contentType.startsWith("text/")) {
onFail(DownloadError.ERROR_FILE_TYPE, null);
return;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java
index a17ecb124..c3b4c0e15 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java
@@ -13,22 +13,24 @@ import de.danoeh.antennapod.core.storage.DBWriter;
*/
public class QueueSorter {
public enum Rule {
- ALPHA_ASC,
- ALPHA_DESC,
+ EPISODE_TITLE_ASC,
+ EPISODE_TITLE_DESC,
DATE_ASC,
DATE_DESC,
DURATION_ASC,
- DURATION_DESC
+ DURATION_DESC,
+ FEED_TITLE_ASC,
+ FEED_TITLE_DESC
}
public static void sort(final Context context, final Rule rule, final boolean broadcastUpdate) {
Comparator<FeedItem> comparator = null;
switch (rule) {
- case ALPHA_ASC:
+ case EPISODE_TITLE_ASC:
comparator = (f1, f2) -> f1.getTitle().compareTo(f2.getTitle());
break;
- case ALPHA_DESC:
+ case EPISODE_TITLE_DESC:
comparator = (f1, f2) -> f2.getTitle().compareTo(f1.getTitle());
break;
case DATE_ASC:
@@ -60,6 +62,12 @@ public class QueueSorter {
return -1 * (duration1 - duration2);
};
break;
+ case FEED_TITLE_ASC:
+ comparator = (f1, f2) -> f1.getFeed().getTitle().compareTo(f2.getFeed().getTitle());
+ break;
+ case FEED_TITLE_DESC:
+ comparator = (f1, f2) -> f2.getFeed().getTitle().compareTo(f1.getFeed().getTitle());
+ break;
default:
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index 217655bf4..c0b417b81 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -258,14 +258,15 @@ public abstract class PlaybackController {
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
- playbackService = ((PlaybackService.LocalBinder) service)
- .getService();
- if (!released) {
- queryService();
- Log.d(TAG, "Connection to Service established");
- } else {
- Log.i(TAG, "Connection to playback service has been established, " +
- "but controller has already been released");
+ if(service instanceof PlaybackService.LocalBinder) {
+ playbackService = ((PlaybackService.LocalBinder) service).getService();
+ if (!released) {
+ queryService();
+ Log.d(TAG, "Connection to Service established");
+ } else {
+ Log.i(TAG, "Connection to playback service has been established, " +
+ "but controller has already been released");
+ }
}
}
diff --git a/core/src/main/res/values-id/strings.xml b/core/src/main/res/values-id/strings.xml
index d91fd609b..383605ded 100644
--- a/core/src/main/res/values-id/strings.xml
+++ b/core/src/main/res/values-id/strings.xml
@@ -1,14 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Activitiy and fragment titles-->
+ <string name="add_feed_label">Tambah Podcast</string>
+ <string name="episodes_label">Kisah</string>
<string name="all_episodes_short_label">Semua</string>
+ <string name="favorite_episodes_label">Favorit</string>
+ <string name="new_label">Baru</string>
<string name="settings_label">Pengaturan</string>
+ <string name="add_new_feed_label">Tambah Podcast</string>
<string name="downloads_label">Unduhan</string>
+ <string name="subscriptions_label">Abonemen</string>
+ <string name="subscriptions_list_label">Daftar Abonemen</string>
+ <string name="cancel_download_label">Batal\nUnduhan</string>
<string name="gpodnet_main_label">gpodder.net</string>
<!--Statistics fragment-->
<!--Main activity-->
<string name="drawer_open">Buka daftar</string>
<string name="drawer_close">Tutup daftar</string>
+ <string name="drawer_feed_counter_new_unplayed">Nomor kisah baru dan kisah yang belum diputar</string>
+ <string name="drawer_feed_counter_new">Nomor kisah baru</string>
+ <string name="drawer_feed_counter_unplayed">Nomor kisah yang belum diputar</string>
<!--Webview actions-->
<string name="copy_url_label">Salin URL</string>
<string name="share_url_label">Bagikan URL</string>
@@ -17,16 +28,24 @@
<string name="cancel_label">Batal</string>
<string name="yes">Ya</string>
<string name="no">Tidak</string>
+ <string name="author_label">Penulis</string>
<string name="language_label">Bahasa</string>
<string name="url_label">URL</string>
<string name="podcast_settings_label">Pengaturan</string>
<string name="cover_label">Gambar</string>
<string name="refresh_label">Segarkan</string>
<string name="description_label">Deskripsi</string>
+ <string name="most_recent_prefix">Kisah terbaru:\u0020</string>
+ <string name="episodes_suffix">\u0020kisah</string>
<string name="save_username_password_label">Simpan nama pengguna dan kata sandi</string>
<string name="close_label">Tutup</string>
+ <string name="send_label">Kirim…</string>
<!--'Add Feed' Activity labels-->
<!--Actions on feeds-->
+ <string name="mark_all_read_label">Tandai semua diputar</string>
+ <string name="mark_all_read_msg">Tandai semua Kisah diputar</string>
+ <string name="mark_all_seen_label">Tandai semua dilihat</string>
+ <string name="remove_feed_label">Hapus Podcast</string>
<string name="share_label">Bagikan...</string>
<string name="share_link_label">Bagikan Tautan</string>
<string name="hide_unplayed_episodes_label">Tidak diputar</string>
@@ -41,6 +60,9 @@
<string name="stop_label">Henti</string>
<string name="remove_label">Hapus</string>
<string name="remove_episode_lable">Hapus Episode</string>
+ <string name="mark_read_label">Tandai diputar</string>
+ <string name="marked_as_read_label">Ditandai diputar</string>
+ <string name="mark_unread_label">Ditandai belum diputar</string>
<string name="add_to_queue_label">Tambah ke Antrian</string>
<string name="added_to_queue_label">Ditambah ke Antrian</string>
<string name="remove_from_queue_label">Hapus dari Antrian</string>
diff --git a/core/src/main/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml
index 213a6655a..b52ccc5fa 100644
--- a/core/src/main/res/values-it-rIT/strings.xml
+++ b/core/src/main/res/values-it-rIT/strings.xml
@@ -1,35 +1,35 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Activitiy and fragment titles-->
- <string name="app_name">AntennaPod</string>
<string name="feeds_label">Feed</string>
+ <string name="statistics_label">Statistiche</string>
<string name="add_feed_label">Aggiungi un podcast</string>
- <string name="podcasts_label">PODCAST</string>
<string name="episodes_label">Episodi</string>
- <string name="new_episodes_label">Episodi nuovi</string>
- <string name="all_episodes_label">Tutti gli episodi</string>
<string name="all_episodes_short_label">Tutti</string>
<string name="favorite_episodes_label">Preferiti</string>
<string name="new_label">Nuovo</string>
- <string name="waiting_list_label">Lista d\'attesa</string>
<string name="settings_label">Impostazioni</string>
<string name="add_new_feed_label">Aggiungi podcast</string>
<string name="downloads_label">Download</string>
<string name="downloads_running_label">In esecuzione</string>
<string name="downloads_completed_label">Completati</string>
<string name="downloads_log_label">Registro</string>
+ <string name="subscriptions_label">Iscrizioni</string>
+ <string name="subscriptions_list_label">Lista Iscrizioni</string>
<string name="cancel_download_label">Annulla download</string>
<string name="playback_history_label">Cronologia delle riproduzioni</string>
<string name="gpodnet_main_label">gpodder.net</string>
<string name="gpodnet_auth_label">gpodder.net login</string>
- <!--New episodes fragment-->
- <string name="recently_published_episodes_label">Pubblicati di recente</string>
- <string name="episode_filter_label">Mostra solo gli episodi nuovi</string>
+ <string name="episode_cache_full_title">Cache degli episodi piena</string>
+ <string name="episode_cache_full_message">Il limite della cache degli episodi è stato raggiunto. Puoi incrementare la dimensione della cache nelle Impostazioni.</string>
<!--Statistics fragment-->
+ <string name="total_time_listened_to_podcasts">Tempo totale di riproduzione podcast:</string>
+ <string name="statistics_details_dialog">%1$d di %2$d episodi iniziati.\n\nRiprodotti %3$s di %4$s.</string>
<!--Main activity-->
<string name="drawer_open">Apri il menù</string>
<string name="drawer_close">Chiudi il menù</string>
<string name="drawer_feed_order_alphabetical">Ordina alfabeticamente</string>
+ <string name="drawer_feed_order_last_update">Ordina per data di pubblicazione</string>
<string name="drawer_feed_counter_new_unplayed">Numero di episodi nuovi e non riprodotti</string>
<string name="drawer_feed_counter_new">Numero di episodi nuovi</string>
<string name="drawer_feed_counter_unplayed">Numero di episodi non riprodotti</string>
@@ -64,16 +64,19 @@
<string name="length_prefix">Durata:\u0020</string>
<string name="size_prefix">Dimensione:\u0020</string>
<string name="processing_label">Elaborazione in corso</string>
+ <string name="loading_label">Caricamento...</string>
<string name="save_username_password_label">Salva nome utente e password</string>
<string name="close_label">Chiudi</string>
<string name="retry_label">Riprova</string>
<string name="auto_download_label">Includi nei download automatici</string>
<string name="auto_download_apply_to_items_title">Applica ai Precedenti Episodi</string>
+ <string name="auto_delete_label">Elimina Episodi Automaticamente</string>
<string name="parallel_downloads_suffix">\u0020download paralleli</string>
- <string name="feed_auto_download_global">Globale</string>
<string name="feed_auto_download_always">Sempre</string>
<string name="feed_auto_download_never">Mai</string>
+ <string name="send_label">Invia...</string>
<string name="episode_cleanup_never">Mai</string>
+ <string name="episode_cleanup_queue_removal">Quando non è in coda</string>
<plurals name="episode_cleanup_days_after_listening">
<item quantity="one">1 giorno dopo il completamento</item>
<item quantity="other">%d giorni dopo il completamento</item>
@@ -88,6 +91,8 @@
<!--Actions on feeds-->
<string name="mark_all_read_label">Segna tutti come riprodotti</string>
<string name="mark_all_read_msg">Segnati tutti gli episodi come riprodotti</string>
+ <string name="mark_all_read_confirmation_msg">Per favore conferma che vuoi segnare tutti gli episodi come riprodotti.</string>
+ <string name="mark_all_read_feed_confirmation_msg">Per favore conferma che vuoi segnare tutti gli episodi di questo feed come riprodotti.</string>
<string name="mark_all_seen_label">Segna tutti come visti</string>
<string name="show_info_label">Informazioni</string>
<string name="remove_feed_label">Rimuovi un podcast</string>
@@ -95,12 +100,13 @@
<string name="share_link_label">Condividi il link al sito</string>
<string name="share_link_with_position_label">Condividi il Link con la Posizione</string>
<string name="share_feed_url_label">Condividi URL del Feed</string>
- <string name="share_item_url_label">Condividi URL dell\'Episodio</string>
- <string name="share_item_url_with_position_label">Condividi l\'URL dell\'Episodio con la Posizione</string>
+ <string name="share_item_url_label">Condividi URL del File dell\'episodio</string>
+ <string name="share_item_url_with_position_label">Condividi l\'URL del File dell\'epsiodio con la Posizione</string>
<string name="feed_delete_confirmation_msg">Per favore conferma la cancellazione di questo feed e di TUTTI gli episodi collegati che sono stati precedentemente scaricati.</string>
<string name="feed_remover_msg">Rimozione feed</string>
<string name="load_complete_feed">Ricarica il feed completo</string>
<string name="hide_episodes_title">Nascondi gli episodi</string>
+ <string name="episode_actions">Applica azioni</string>
<string name="hide_unplayed_episodes_label">Non riprodotti</string>
<string name="hide_paused_episodes_label">In pausa</string>
<string name="hide_played_episodes_label">Riprodotti</string>
@@ -110,6 +116,7 @@
<string name="hide_not_downloaded_episodes_label">Non scaricati</string>
<string name="filtered_label">Filtrati</string>
<string name="refresh_failed_msg">{fa-exclamation-circle} Ultimo aggiornamento fallito</string>
+ <string name="open_podcast">Apri Podcast</string>
<!--actions on feeditems-->
<string name="download_label">Download</string>
<string name="play_label">Riproduci</string>
@@ -130,8 +137,6 @@
<string name="removed_from_favorites">Rimosso dai Preferiti</string>
<string name="visit_website_label">Visita il sito</string>
<string name="support_label">Carica questo su Flattr</string>
- <string name="enqueue_all_new">Accoda tutti</string>
- <string name="download_all">Scarica tutti</string>
<string name="skip_episode_label">Salta l\'episodio</string>
<string name="activate_auto_download">Attiva il download automatico</string>
<string name="deactivate_auto_download">Disattiva il download automatico</string>
@@ -152,6 +157,8 @@
<string name="download_error_connection_error">Errore di connessione</string>
<string name="download_error_unknown_host">Host sconosciuto</string>
<string name="download_error_unauthorized">Errore di autenticazione</string>
+ <string name="download_error_file_type_type">Errore Formato FIle</string>
+ <string name="download_error_forbidden">Proibito</string>
<string name="cancel_all_downloads_label">Annulla tutti i download</string>
<string name="download_canceled_msg">Download annullato</string>
<string name="download_report_title">Download completato con un errore (o errori)</string>
@@ -160,6 +167,10 @@
<string name="download_error_io_error">Errore IO</string>
<string name="download_error_request_error">Errore della richiesta</string>
<string name="download_error_db_access">Errore di accesso al database</string>
+ <plurals name="downloads_left">
+ <item quantity="one">%d download rimanente</item>
+ <item quantity="other">%d download rimanenti</item>
+ </plurals>
<string name="downloads_processing">Elaborazione dei download in corso</string>
<string name="download_notification_title">Download podcast in corso</string>
<string name="download_report_content">%1$d download con successo, %2$d falliti</string>
@@ -182,7 +193,6 @@
<string name="playback_error_server_died">Server morto</string>
<string name="playback_error_unknown">Errore sconosciuto</string>
<string name="no_media_playing_label">Nessun elemento multimediale in riproduzione</string>
- <string name="position_default_label">00:00:00</string>
<string name="player_buffering_msg">Buffer in corso</string>
<string name="playbackservice_notification_title">Riproduzione del podcast in corso</string>
<string name="unknown_media_key">AntennaPod - Chiave dell\'elemento multimediale sconosciuta: %1$d</string>
@@ -238,6 +248,7 @@
<string name="no_feeds_label">Non sei ancora abbonato a nessun feed.</string>
<string name="no_chapters_label">Questo episodio non ha capitoli.</string>
<!--Preferences-->
+ <string name="project_pref">Progetto</string>
<string name="other_pref">Altro</string>
<string name="about_pref">Informazioni</string>
<string name="queue_label">Coda</string>
@@ -251,6 +262,8 @@
<string name="network_pref">Rete</string>
<string name="pref_autoUpdateIntervallOrTime_Disable">Disabilita</string>
<string name="pref_autoUpdateIntervallOrTime_Interval">Imposta Intervallo</string>
+ <string name="pref_autoUpdateIntervallOrTime_every">ogni %1$s</string>
+ <string name="pref_autoUpdateIntervallOrTime_at">alle %1$s</string>
<string name="pref_downloadMediaOnWifiOnly_sum">Abilita il download dei media solo tramite WiFi</string>
<string name="pref_followQueue_title">Playback continuo</string>
<string name="pref_downloadMediaOnWifiOnly_title">Download dei media su WiFi</string>
@@ -291,6 +304,8 @@
<string name="pref_gpodnet_logout_toast">Logout effettuato</string>
<string name="pref_gpodnet_setlogin_information_title">Cambia le informazioni di login</string>
<string name="pref_gpodnet_setlogin_information_sum">Cambia le informazioni di login per il tuo account gpodder.net.</string>
+ <string name="pref_gpodnet_sync_title">Sincronizza ora</string>
+ <string name="pref_gpodnet_sync_started">Sincronizzazione avviata</string>
<string name="pref_playback_speed_title">Velocità di riproduzione</string>
<string name="pref_playback_speed_sum">Personalizza le velocità disponibili per la riproduzione audio a velocità variabile</string>
<string name="pref_gpodnet_sethostname_title">Imposta l\'hostname</string>
@@ -307,6 +322,9 @@
<string name="send_email">Invia e-mail</string>
<string name="experimental_pref">Sperimentale</string>
<string name="pref_current_value">Valore corrente: %1$s</string>
+ <string name="pref_proxy_title">Proxy</string>
+ <string name="pref_faq">FAQ</string>
+ <string name="pref_cast_title">Supporto a Chromecast</string>
<!--Auto-Flattr dialog-->
<string name="auto_flattr_enable">Abilita l\'esecuzione automatica di Flattr</string>
<string name="auto_flattr_after_percent">Carica l\'episodio su Flattr appena è stato riprodotto al %d percento</string>
@@ -325,6 +343,7 @@
<string name="opml_import_label">Importazione OPML</string>
<string name="opml_directory_error">ERRORE!</string>
<string name="reading_opml_label">Lettura OPML file in corso</string>
+ <string name="opml_import_error_no_file">Nessun file selezionato!</string>
<string name="select_all_label">Seleziona tutti</string>
<string name="deselect_all_label">Deseleziona tutti</string>
<string name="choose_file_from_filesystem">Dal filesystem locale</string>
@@ -393,6 +412,9 @@
<string name="create_folder_error_no_write_access">Impossibile scrivere in questa directory</string>
<string name="create_folder_error_already_exists">La cartella esiste già</string>
<string name="create_folder_error">Non è stato possibile creare la cartella</string>
+ <string name="folder_does_not_exist_error">\"%1$s\" non esiste</string>
+ <string name="folder_not_readable_error">\"%1$s\" non è leggibile</string>
+ <string name="folder_not_writable_error">\"%1$s\" non è scrivibile</string>
<string name="folder_not_empty_dialog_title">La cartella non è vuota</string>
<string name="folder_not_empty_dialog_msg">La cartella che hai selezionato non è vuota. I download dei media e altri file saranno creati in questa cartella. Continuare?</string>
<string name="set_to_default_folder">Scegli la cartella predefinita</string>
@@ -403,22 +425,13 @@
<string name="subscribe_label">Abbonati</string>
<string name="subscribed_label">Abbonato</string>
<!--Content descriptions for image buttons-->
- <string name="show_chapters_label">Mostra i capitoli</string>
- <string name="show_shownotes_label">Mostra le note dell\'episodio</string>
- <string name="show_cover_label">Mosta l\'immagine</string>
<string name="rewind_label">Riavvolgi</string>
<string name="fast_forward_label">Avanti veloce</string>
<string name="media_type_audio_label">Audio</string>
<string name="media_type_video_label">Video</string>
<string name="navigate_upwards_label">Naviga verso l\'alto</string>
- <string name="butAction_label">Ulteriori azioni</string>
- <string name="status_playing_label">L\'episodio è in riproduzione</string>
<string name="status_downloading_label">L\'episodio sta venendo scaricato</string>
- <string name="status_downloaded_label">L\'episodio è stato scaricato</string>
- <string name="status_unread_label">L\'oggetto è nuovo</string>
<string name="in_queue_label">L\'episodio è in coda</string>
- <string name="new_episodes_count_label">Numero di episodi nuovi</string>
- <string name="in_progress_episodes_count_label">Numero di episodi che hai iniziato ad ascoltare</string>
<string name="drag_handle_content_description">Trascina per cambiare la posizione di questo oggetto</string>
<string name="load_next_page_label">Carica la pagina successiva</string>
<!--Feed information screen-->
@@ -431,6 +444,8 @@
<!--AntennaPodSP-->
<string name="sp_apps_importing_feeds_msg">Importazione di sottoscrizioni da applicazioni monouso in corso...</string>
<string name="search_itunes_label">Cerca su iTunes</string>
+ <string name="filter">Filtro</string>
+ <!--Episodes apply actions-->
<string name="all_label">Tutti</string>
<string name="selected_all_label">Tutti gli Episodi Selezionati</string>
<string name="none_label">Nessuno</string>
@@ -441,6 +456,7 @@
<string name="not_downloaded_label">Non scaricati</string>
<string name="queued_label">In coda</string>
<string name="not_queued_label">Non in coda</string>
+ <!--Sort-->
<string name="sort_title_a_z">Titolo (A \u2192 Z)</string>
<string name="sort_title_z_a">Titolo (Z \u2192 A)</string>
<string name="sort_date_new_old">Data (New \u2192 Old)</string>
@@ -448,8 +464,18 @@
<string name="sort_duration_short_long">Durata (Short \u2192 Long)</string>
<string name="sort_duration_long_short">Durata (Long \u2192 Short)</string>
<!--Rating dialog-->
+ <string name="rating_title">Ti piace AntennaPod?</string>
+ <string name="rating_later_label">Ricordamelo più tardi</string>
<!--Audio controls-->
<string name="volume">Volume</string>
<string name="audio_effects">Effetti Audio</string>
<!--proxy settings-->
+ <string name="proxy_type_label">Tipo</string>
+ <string name="host_label">Host</string>
+ <string name="port_label">Porta</string>
+ <string name="optional_hint">(Opzionale)</string>
+ <string name="proxy_port_invalid_error">Porta non valida</string>
+ <!--Casting-->
+ <string name="cast_media_route_menu_title">Riproduci su...</string>
+ <!--<string name="cast_failed_to_connect">Could not connect to the device</string>-->
</resources>
diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml
index 561771a2f..29ddd7e73 100644
--- a/core/src/main/res/values-ko/strings.xml
+++ b/core/src/main/res/values-ko/strings.xml
@@ -2,6 +2,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Activitiy and fragment titles-->
<string name="feeds_label">피드</string>
+ <string name="statistics_label">통계</string>
<string name="add_feed_label">팟캐스트 추가</string>
<string name="episodes_label">에피소드</string>
<string name="all_episodes_short_label">모두</string>
@@ -13,6 +14,8 @@
<string name="downloads_running_label">실행 중</string>
<string name="downloads_completed_label">마침</string>
<string name="downloads_log_label">기록</string>
+ <string name="subscriptions_label">구독</string>
+ <string name="subscriptions_list_label">구독 목록</string>
<string name="cancel_download_label">다운로드 취소</string>
<string name="playback_history_label">재생 기록</string>
<string name="gpodnet_main_label">gpodder.net</string>
@@ -21,6 +24,8 @@
<string name="episode_cache_full_title">에피소드 캐시 꽉 참</string>
<string name="episode_cache_full_message">에피소드 캐시 한계값에 도달했습니다. 설정에서 캐시 크기를 늘릴 수 있습니다.</string>
<!--Statistics fragment-->
+ <string name="total_time_listened_to_podcasts">재생한 팟캐스트의 전체 시간</string>
+ <string name="statistics_details_dialog">에피소드 %1$d개 (전체 %2$d개) 시작.\n\n%3$s개 재생 (전체 %4$s개).</string>
<!--Main activity-->
<string name="drawer_open">메뉴 열기</string>
<string name="drawer_close">메뉴 닫기</string>
@@ -69,7 +74,9 @@
<string name="auto_download_label">자동 다운로드에 포함</string>
<string name="auto_download_apply_to_items_title">예전 에피소드에 적용</string>
<string name="auto_download_apply_to_items_message">새로운 \'자동 다운로드\' 설정은 새 에피소드에 자동적으로 적용됩니다. 기존에 배포된 에피소드에도 적용할까요?</string>
+ <string name="auto_delete_label">에피소드 자동 삭제</string>
<string name="parallel_downloads_suffix">개 동시 다운로드</string>
+ <string name="feed_auto_download_global">전체 기본값</string>
<string name="feed_auto_download_always">항상</string>
<string name="feed_auto_download_never">안 함</string>
<string name="send_label">보내기…</string>
@@ -98,6 +105,8 @@
<string name="share_link_label">홈페이지 링크 공유</string>
<string name="share_link_with_position_label">위치와 같이 링크 공유</string>
<string name="share_feed_url_label">피드 URL 공유</string>
+ <string name="share_item_url_label">에피소드 파일 URL 공유</string>
+ <string name="share_item_url_with_position_label">에피소드 파일 URL과 재생 위치 공유</string>
<string name="feed_delete_confirmation_msg">이 피드와 이 피드에서 다운로드한 모든 에피소드를 삭제하시려면 확인을 누르십시오.</string>
<string name="feed_remover_msg">피드 삭제하는 중</string>
<string name="load_complete_feed">전체 피드 새로고침</string>
@@ -112,6 +121,7 @@
<string name="hide_not_downloaded_episodes_label">다운로드 안 함</string>
<string name="filtered_label">필터링함</string>
<string name="refresh_failed_msg">{fa-exclamation-circle} 최근 새로 고침 실패</string>
+ <string name="open_podcast">팟캐스트 열기</string>
<!--actions on feeditems-->
<string name="download_label">다운로드</string>
<string name="play_label">재생</string>
@@ -152,6 +162,8 @@
<string name="download_error_connection_error">연결 오류</string>
<string name="download_error_unknown_host">알 수 없는 호스트</string>
<string name="download_error_unauthorized">인증 오류</string>
+ <string name="download_error_file_type_type">파일 종류 오류</string>
+ <string name="download_error_forbidden">금지됨</string>
<string name="cancel_all_downloads_label">모든 다운로드 취소</string>
<string name="download_canceled_msg">다운로드 취소함</string>
<string name="download_canceled_autodownload_enabled_msg">다운로드 취소함\n이 항목에 <i>자동 다운로드</i>를 해제합니다</string>
@@ -244,7 +256,10 @@
<string name="no_items_label">이 목록에 항목이 없습니다.</string>
<string name="no_feeds_label">아직 어떤 피드도 구독하지 않았습니다.</string>
<string name="no_chapters_label">에피소드에 챕터가 없습니다.</string>
+ <string name="no_shownotes_label">이 에피소드에는 프로그램 메모가 없습니다.</string>
<!--Preferences-->
+ <string name="storage_pref">저장소</string>
+ <string name="project_pref">프로젝트</string>
<string name="other_pref">기타</string>
<string name="about_pref">정보</string>
<string name="queue_label">대기열</string>
@@ -323,6 +338,10 @@
<string name="pref_gpodnet_logout_toast">로그아웃 성공</string>
<string name="pref_gpodnet_setlogin_information_title">로그인 정보 바꾸기</string>
<string name="pref_gpodnet_setlogin_information_sum">gpodder.net 계정의 로그인 정보를 바꿉니다.</string>
+ <string name="pref_gpodnet_sync_title">지금 동기화</string>
+ <string name="pref_gpodnet_sync_sum">gpodder.net의 구독과 에피소드 상태 동기화</string>
+ <string name="pref_gpodnet_sync_started">동기화 시작함</string>
+ <string name="pref_gpodnet_login_status"><![CDATA[<i>%1$s</i> 사용자로 로그인, <i>%2$s</i> 장치]]></string>
<string name="pref_playback_speed_title">재생 속도</string>
<string name="pref_playback_speed_sum">여러가지 오디오 재생 속도 직접 설정</string>
<string name="pref_fast_forward">빠르게 감기 시간</string>
@@ -333,6 +352,12 @@
<string name="pref_expandNotify_sum">항상 알림에서 재생 버튼이 표시되도록 확장합니다.</string>
<string name="pref_persistNotify_title">재생 조작 고정</string>
<string name="pref_persistNotify_sum">재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능을 유지합니다.</string>
+ <string name="pref_compact_notification_buttons_title">화면잠금 버튼 설정</string>
+ <string name="pref_compact_notification_buttons_sum">잠금화면의 조작 버튼을 바꿉니다. 재생/일시중지 버튼은 항상 포함됩니다.</string>
+ <string name="pref_compact_notification_buttons_dialog_title">최대 %1$d개 항목 선택</string>
+ <string name="pref_compact_notification_buttons_dialog_error">최대 %1$d개 항모만 선택할 수 있습니다.</string>
+ <string name="pref_show_subscriptions_in_drawer_title">구독 표시</string>
+ <string name="pref_show_subscriptions_in_drawer_sum">네비게이션 드로어에 직접 구독 목록을 표시</string>
<string name="pref_lockscreen_background_title">잠금 화면 배경 설정</string>
<string name="pref_lockscreen_background_sum">현재 에피소드의 이미지를 잠금 화면의 배경으로 설정합니다. 대신 이는 제3자 앱의 이미지도 표시하게 됩니다.</string>
<string name="pref_showDownloadReport_title">다운로드 보고서 보기</string>
@@ -350,6 +375,13 @@
<string name="pref_sonic_title">소닉 미디어 플레이어</string>
<string name="pref_sonic_message">내장 소닉 미디어 플레이어를 안드로이드 고유 미디어 플레이어와 Prestissimo 대신 사용합니다.</string>
<string name="pref_current_value">현재 값: %1$s</string>
+ <string name="pref_proxy_title">프록시</string>
+ <string name="pref_proxy_sum">네트워크 프록시 설정</string>
+ <string name="pref_faq">자주 묻는 질문</string>
+ <string name="pref_known_issues">알려진 문제점</string>
+ <string name="pref_no_browser_found">웹브라우저가 없습니다.</string>
+ <string name="pref_cast_title">크롬캐스트 지원</string>
+ <string name="pref_cast_message">캐스트 장치의 원격 미디어 재생 기능 사용 (예: 크롬캐스트, 안드로이드 TV의 오디오 스피커)</string>
<!--Auto-Flattr dialog-->
<string name="auto_flattr_enable">자동 flattr 사용</string>
<string name="auto_flattr_after_percent">%d 퍼센트를 재생하면 에피소드에 flattr합니다</string>
@@ -372,6 +404,8 @@
<string name="opml_import_label">OPML 가져오기</string>
<string name="opml_directory_error">오류!</string>
<string name="reading_opml_label">OPML 파일을 읽는 중</string>
+ <string name="opml_reader_error">OPML 문서를 읽는데 오류가 발생했습니다:</string>
+ <string name="opml_import_error_no_file">파일을 선택하지 않았습니다!</string>
<string name="select_all_label">모두 선택</string>
<string name="deselect_all_label">모두 선택 해제</string>
<string name="select_options_label">선택…</string>
@@ -424,6 +458,7 @@
<string name="gpodnetauth_device_chooseExistingDevice">기존 장치 선택:</string>
<string name="gpodnetauth_device_errorEmpty">장치 ID는 비어 있으면 안 됩니다</string>
<string name="gpodnetauth_device_errorAlreadyUsed">장치 ID를 이미 사용 중입니다</string>
+ <string name="gpodnetauth_device_caption_errorEmpty">자막이 비어 있으면 안 됩니다</string>
<string name="gpodnetauth_device_butChoose">선택</string>
<string name="gpodnetauth_finish_title">로그인이 성공했습니다!</string>
<string name="gpodnetauth_finish_descr">축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다.</string>
@@ -525,6 +560,31 @@
<string name="stereo_to_mono">다운믹스: 스테레오에서 모노로</string>
<string name="sonic_only">소닉 전용</string>
<!--proxy settings-->
+ <string name="proxy_type_label">종류</string>
+ <string name="host_label">호스트</string>
+ <string name="port_label">포트</string>
+ <string name="optional_hint">(선택 사항)</string>
+ <string name="proxy_test_label">테스트</string>
+ <string name="proxy_checking">확인 중...</string>
+ <string name="proxy_test_successful">테스트 성공</string>
+ <string name="proxy_test_failed">테스트 실패</string>
+ <string name="proxy_host_empty_error">호스트가 비어 있으면 안 됩니다</string>
+ <string name="proxy_host_invalid_error">호스트가 올바른 IP 주소 또는 도메인이 아닙니다</string>
+ <string name="proxy_port_invalid_error">포트가 올바르지 않습니다</string>
<!--Casting-->
+ <string name="cast_media_route_menu_title">다른 장치에서 재생...</string>
+ <string name="cast_disconnect_label">캐스트 세션 연결 끊기</string>
+ <string name="cast_not_castable">선택한 미디어가 캐스트 장치와 호환되지 않습니다</string>
+ <string name="cast_failed_to_play">미디어 재생 시작에 실패했습니다</string>
+ <string name="cast_failed_to_stop">미디어 재생 중지에 실패했습니다</string>
+ <string name="cast_failed_to_pause">미디어 재생 일시 중지에 실패했습니다</string>
<!--<string name="cast_failed_to_connect">Could not connect to the device</string>-->
+ <string name="cast_failed_setting_volume">볼륨 설정에 실패했습니다</string>
+ <string name="cast_failed_no_connection">캐스트 장치에 연결이 없습니다</string>
+ <string name="cast_failed_no_connection_trans">캐스트 장치와 연결이 끊어졌습니다. 다시 연결을 맺으려고 시도하는 중입니다. 몇 초 기다렸다가 다시 시도해 보십시오.</string>
+ <string name="cast_failed_perform_action">동작을 수행하는데 실패했습니다</string>
+ <string name="cast_failed_status_request">캐스트 장치와 동기화하는데 실패했습니다</string>
+ <string name="cast_failed_seek">캐스트 장치에 새 재생 위치로 이동하는데 실패했습니다</string>
+ <string name="cast_failed_receiver_player_error">리시버 플레이어에서 심각한 오류가 발생했습니다</string>
+ <string name="cast_failed_media_error_skipping">미디어 재생에 오류. 건너뜁니다...</string>
</resources>
diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml
index 21a5af0b4..271f23459 100644
--- a/core/src/main/res/values-pt-rBR/strings.xml
+++ b/core/src/main/res/values-pt-rBR/strings.xml
@@ -98,6 +98,7 @@
<string name="mark_all_read_label">Marcar todos como lido</string>
<string name="mark_all_read_msg">Marcar todos Episódios como lidos</string>
<string name="mark_all_read_confirmation_msg">Por favor, confirme que você quer marcar todos os episódios como já tocados.</string>
+ <string name="mark_all_read_feed_confirmation_msg">Por favor confirme se você quer marcar todos os episódios deste feed como já tocados.</string>
<string name="mark_all_seen_label">Marcar todos como lido</string>
<string name="show_info_label">Mostrar informação</string>
<string name="remove_feed_label">Remover Podcast</string>
@@ -188,6 +189,8 @@
<string name="authentication_notification_title">Autenticação requerida</string>
<string name="authentication_notification_msg">O recurso que você requisitou requer um usuário e uma senha</string>
<string name="confirm_mobile_download_dialog_title">Confirmar Download Mobile</string>
+ <string name="confirm_mobile_download_dialog_message_not_in_queue">Baixar sobre plano de dados foi desabilitado nas configurações.\n\nVocê pode escolher entre apenas adicionar o episódio na fila ou você pode permitir o download temporáriamente.\n\n<small>Sua escolha será lembrada por 10 minutos.</small></string>
+ <string name="confirm_mobile_download_dialog_message">Baixar sobre plano de dados foi desabilitado em configurações.\n\n Você quer permitir o download temporáriamente?\n\n<small>Sua escolha será lembrada por 10 minutos</small></string>
<string name="confirm_mobile_download_dialog_only_add_to_queue">Enfileirados</string>
<string name="confirm_mobile_download_dialog_enable_temporarily">Permitir temporariamente</string>
<!--Mediaplayer messages-->
@@ -226,6 +229,7 @@
<string name="return_home_label">Retornar ao início</string>
<string name="flattr_auth_success">Autenticado com sucesso! Agora você poderá utilizar o Flattr de dentro do AntennaPod.</string>
<string name="no_flattr_token_title">Nenhum token do Flattr encontrado</string>
+ <string name="no_flattr_token_notification_msg">Sua conta flattr parece não estar conectada ao AntennaPod. Toque aqui para autenticar.</string>
<string name="no_flattr_token_msg">Sua conta Flattr não está conectada ao AntennaPod. Você pode conectar sua conta ao AntennaPod para usar o Flattr de dentro da aplicação ou pode visitar o website do feed para usar o Flattr por lá.</string>
<string name="authenticate_now_label">Autenticar</string>
<string name="action_forbidden_title">Ação proibida</string>
@@ -247,6 +251,7 @@
<!--Variable Speed-->
<string name="download_plugin_label">Download Plugin</string>
<string name="no_playback_plugin_title">Plugin Não Instalado</string>
+ <string name="no_playback_plugin_or_sonic_msg">Para alterar a velocidade da reprodução, nós recomendamos que você abilite o reprodutor de mídias Sonic [Android 4.1+].\n\nOu então, vocÊ pode baixar o plugin de terceiros <i>Prestissimo</i> da Play Store.\nQualquer problema com Prestissimo não é responsabilidade do AntennaPod e deverá ser reportado para o proprietário do plugin.</string>
<string name="set_playback_speed_label">Velocidades de Reprodução</string>
<string name="enable_sonic">Habilitar Sonic</string>
<!--Empty list labels-->
@@ -265,13 +270,33 @@
<string name="pref_episode_cleanup_title">Limpar Episódio</string>
<string name="pref_episode_cleanup_summary">Episódios que não foram enfileirados e não foram favoritados deveriam ser elegídos para remoção se o Auto Download precisar de espaço para novos episódios</string>
<string name="pref_pauseOnDisconnect_sum">Pausar a exibição quando phones de ouvidos ou bluetooth forem disconectados</string>
+ <string name="pref_unpauseOnHeadsetReconnect_sum">Continuar a exibição quando os fones de ouvido forem reconectados</string>
+ <string name="pref_unpauseOnBluetoothReconnect_sum">Continuar a exibição quando o bluetooth reconectar</string>
+ <string name="pref_hardwareForwardButtonSkips_title">Botão avançar pula</string>
+ <string name="pref_hardwareForwardButtonSkips_sum">Quando pressionar um botão físico, avançar para o próximo episódio ao invés de avançar rápido</string>
<string name="pref_followQueue_sum">Pular para próximo item da fila quando a reprodução terminar</string>
+ <string name="pref_auto_delete_sum">Apagar os episódios quando a exibição for concluída</string>
+ <string name="pref_auto_delete_title">Deletar automaticamente</string>
+ <string name="pref_smart_mark_as_played_sum">Marcar episódios como escutados </string>
+ <string name="pref_smart_mark_as_played_title">Inteligentemente marcar como escutado </string>
+ <string name="pref_skip_keeps_episodes_sum">Manter os episódios quando eles forem avançados</string>
+ <string name="pref_skip_keeps_episodes_title">Manter episódios avançados</string>
<string name="playback_pref">Reprodução</string>
<string name="network_pref">Rede</string>
+ <string name="pref_autoUpdateIntervallOrTime_title">Intervado de atualização ou Tempo do dia</string>
+ <string name="pref_autoUpdateIntervallOrTime_sum">Especifique um intervalo ou um tempo específico do dia para atualizar os seus feed automaticamente</string>
+ <string name="pref_autoUpdateIntervallOrTime_message">Você pode configurar um <i>intervalo</i> como \"cada 2 horas\", configurar um específico <i>horário do dia</i> como \"7:00 AM\" ou <i>desabilitar</i> atualizações automaticas completamente.\n\n<small>Observe: Horários de atualização não são precisos. Você deve considar um possível atraso.</small></string>
+ <string name="pref_autoUpdateIntervallOrTime_Disable">Desabilitar</string>
+ <string name="pref_autoUpdateIntervallOrTime_Interval">Configurar Intervalo</string>
+ <string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Configurar Tempo do dia</string>
+ <string name="pref_autoUpdateIntervallOrTime_every">cada %1$s</string>
+ <string name="pref_autoUpdateIntervallOrTime_at">às %1$s</string>
<string name="pref_downloadMediaOnWifiOnly_sum">Fazer download dos arquivos apenas via rede WiFi</string>
<string name="pref_followQueue_title">Reprodução contínua</string>
<string name="pref_downloadMediaOnWifiOnly_title">Download de mídia via WiFi</string>
<string name="pref_pauseOnHeadsetDisconnect_title">Fones de ouvido desconectados</string>
+ <string name="pref_unpauseOnHeadsetReconnect_title">Fones de ouvido reconectados</string>
+ <string name="pref_unpauseOnBluetoothReconnect_title">Bluetooth reconectados</string>
<string name="pref_mobileUpdate_title">Atualizações via Rede de Dados Celular</string>
<string name="pref_mobileUpdate_sum">Permite atualizações quando conectado na rede de dados celular</string>
<string name="refreshing_label">Atualizando</string>
@@ -282,13 +307,26 @@
<string name="pref_flattr_this_app_sum">Suportar o desenvolvimento do AntennaPod usando o Flattr. Obrigado!</string>
<string name="pref_revokeAccess_title">Revogar acesso</string>
<string name="pref_revokeAccess_sum">Cancelar permissão de acesso à sua conta Flattr</string>
+ <string name="pref_auto_flattr_title">Flattr automático</string>
+ <string name="pref_auto_flattr_sum">Configurar automaticamente com flattr</string>
<string name="user_interface_label">Interface com usuário</string>
<string name="pref_set_theme_title">Selecionar tema</string>
+ <string name="pref_nav_drawer_title">Customizar Gaveta de Navegação</string>
+ <string name="pref_nav_drawer_sum">Customizar a aparencia da gaveta de navegação.</string>
+ <string name="pref_nav_drawer_items_title">Configurar items a Gaveta de Navegação</string>
+ <string name="pref_nav_drawer_items_sum">Escolher quais items irão aparecer na gaveta de navegação.</string>
+ <string name="pref_nav_drawer_feed_order_title">Configurar Ordem de Assinaturas</string>
+ <string name="pref_nav_drawer_feed_order_sum">Mudar a ordem de suas assinaturas</string>
+ <string name="pref_nav_drawer_feed_counter_title">Configurar um contador de assinaturas</string>
+ <string name="pref_nav_drawer_feed_counter_sum">Mudar a informação exibida pelo contador de assinaturas</string>
<string name="pref_set_theme_sum">Altera a aparência do AntennaPod</string>
<string name="pref_automatic_download_title">Download automático</string>
<string name="pref_automatic_download_sum">Configurar download automático de episódios.</string>
<string name="pref_autodl_wifi_filter_title">Habilitar filtro Wi-Fi</string>
<string name="pref_autodl_wifi_filter_sum">Permitir download automático somente pelas redes Wi-Fi selecionadas.</string>
+ <string name="pref_automatic_download_on_battery_title">Baixar enquanto não está carregando</string>
+ <string name="pref_automatic_download_on_battery_sum">Permitir download automático enquanto a bateria não está carregando</string>
+ <string name="pref_parallel_downloads_title">Downloads paralelos</string>
<string name="pref_episode_cache_title">Cache de episódios</string>
<string name="pref_theme_title_light">Claro</string>
<string name="pref_theme_title_dark">Escuro</string>
@@ -302,11 +340,55 @@
<string name="pref_gpodnet_logout_toast">Saiu com sucesso</string>
<string name="pref_gpodnet_setlogin_information_title">Alterar informações de login</string>
<string name="pref_gpodnet_setlogin_information_sum">Alterar informações de login da sua conta gpodder.net</string>
+ <string name="pref_gpodnet_sync_title">Sincronizar agora</string>
+ <string name="pref_gpodnet_sync_sum">Sincronizar assinaturas e estados dos episódios com gpodder.net</string>
+ <string name="pref_gpodnet_sync_started">Sincronização iniciada</string>
+ <string name="pref_gpodnet_login_status"><![CDATA[Entrou como <i>%1$s</i> com o dispositivo <i>%2$s</i>]]></string>
<string name="pref_playback_speed_title">Velocidades de Reprodução</string>
<string name="pref_playback_speed_sum">Personalize as velocidades variáveis de reprodução de áudio.</string>
+ <string name="pref_fast_forward">Avançar o tempo</string>
+ <string name="pref_rewind">Voltar o tempo</string>
<string name="pref_gpodnet_sethostname_title">Configurar hostname</string>
<string name="pref_gpodnet_sethostname_use_default_host">Usar host padrão</string>
+ <string name="pref_expandNotify_title">Expandir Notificação</string>
+ <string name="pref_expandNotify_sum">Sempre expandir a notificação para mostrar os botões de reprodução.</string>
+ <string name="pref_persistNotify_title">Controles de Reprodução Persistentes</string>
+ <string name="pref_persistNotify_sum">Manter a notificação e controles na tela de bloqueio enquanto a reprodução está pausada.</string>
+ <string name="pref_compact_notification_buttons_title">Configurar Botões da tela de bloqueio</string>
+ <string name="pref_compact_notification_buttons_sum">Mudar os botões de reprodução na tela de bloqueio. O botão tocar/pausar sempre é incluso.</string>
+ <string name="pref_compact_notification_buttons_dialog_title">Selecione no máximo %1$d itens</string>
+ <string name="pref_compact_notification_buttons_dialog_error">Você só pode selecionar no máximo %1$d itens.</string>
+ <string name="pref_show_subscriptions_in_drawer_title">Mostrar Assinaturas</string>
+ <string name="pref_show_subscriptions_in_drawer_sum">Mostrar lista de assinaturas diretamente da gaveta de navegação</string>
+ <string name="pref_lockscreen_background_title">Configurar plano de fundo da tela de bloqueio</string>
+ <string name="pref_lockscreen_background_sum">Configurar o plano de fundo da tela de bloqueio para a imagem do episódio atual. Como um efeito colateral, também ira mostrar imagens de aplicativos de terceiros.</string>
+ <string name="pref_showDownloadReport_title">Mostrar Relatório de Downloads</string>
+ <string name="pref_showDownloadReport_sum">Se os downloads falharem, gerar um relatório que mostra os detalhes da falha.</string>
+ <string name="pref_expand_notify_unsupport_toast">Versões do Android inferiores a 4.1 não suportam notificações expansíveis</string>
+ <string name="pref_queueAddToFront_sum">Adicionar um novo episódio para a frente da fila.</string>
+ <string name="pref_queueAddToFront_title">Enfileirar para a frente</string>
+ <string name="pref_smart_mark_as_played_disabled">Desabilitado</string>
+ <string name="pref_image_cache_size_title">Tamanho da Imagem em Cache</string>
+ <string name="pref_image_cache_size_sum">Tamanho do cache de disco para imagens.</string>
+ <string name="crash_report_title">Relatório de Falha</string>
+ <string name="crash_report_sum">Enviar o relatório da última falha por e-mail</string>
+ <string name="send_email">Enviar e-mail</string>
+ <string name="experimental_pref">Experimental</string>
+ <string name="pref_sonic_title">Reprodutor de mídia Sonic</string>
+ <string name="pref_sonic_message">Utilizar o reprodutor de mídia Sonic no lugar do reprodutor de mídia nativo do Android e do Prestissimo</string>
+ <string name="pref_current_value">Valor atual: %1$s</string>
+ <string name="pref_proxy_title">Proxy</string>
+ <string name="pref_proxy_sum">Configurar um proxy da rede</string>
+ <string name="pref_faq">FAQ</string>
+ <string name="pref_known_issues">Problemas conhecidos</string>
+ <string name="pref_no_browser_found">Nenhum navegador web encontrado.</string>
+ <string name="pref_cast_title">Suporte ao Chromecast</string>
+ <string name="pref_cast_message">Abilitar o suporte de reprodução de mídia remota em dispositivos Cast ( assim como Chromecast, Caixas de som ou Android TV)</string>
<!--Auto-Flattr dialog-->
+ <string name="auto_flattr_enable">Abilitar automaticamente o flattr</string>
+ <string name="auto_flattr_after_percent">Episódio Flattr assim que %d porcento for tocado</string>
+ <string name="auto_flattr_ater_beginning">Episódio Flattr quando a repodução iniciar</string>
+ <string name="auto_flattr_ater_end">Episódio Flattr quando a reprodução finalizar</string>
<!--Search-->
<string name="search_hint">Procurar por Feeds ou Episódios</string>
<string name="found_in_shownotes_label">Encontrado nas notas do podcast</string>
@@ -316,15 +398,27 @@
<string name="found_in_title_label">Encontrado no título</string>
<!--OPML import and export-->
<string name="opml_import_txtv_button_lable">Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro.</string>
+ <string name="opml_import_option">Opção %1$d</string>
+ <string name="opml_import_explanation_1">Escolha o caminho específico de um arquivo no sistema.</string>
+ <string name="opml_import_explanation_2">Utilizar uma aplicação externa como Dropbox, Google Drive ou o seu gerenciador de arquivos favoritos para abrir um arquivo OPML.</string>
+ <string name="opml_import_explanation_3">Muitos aplicativos como Google Mail, Dropbox, Google Drive e outros gerenciadores podem <i>abrir</i> arquivos OPML <i>com</i> AntennaPod.</string>
<string name="start_import_label">Iniciar importação</string>
<string name="opml_import_label">Importação de OPML</string>
<string name="opml_directory_error">ERRO!</string>
<string name="reading_opml_label">Lendo arquivo OPML</string>
+ <string name="opml_reader_error">Um erro ocorreu ao ler o documento OPML:</string>
+ <string name="opml_import_error_no_file">Nenhum arquivo selecionado!</string>
<string name="select_all_label">Selecionar todos</string>
<string name="deselect_all_label">Remover seleção</string>
+ <string name="select_options_label">Selecionar...</string>
+ <string name="choose_file_from_filesystem">Dos arquivos locais do sistema</string>
+ <string name="choose_file_from_external_application">Utilizar aplicação externa</string>
<string name="opml_export_label">Exportar OPML</string>
+ <string name="exporting_label">Exportando...</string>
<string name="export_error_label">Erro na exportação</string>
+ <string name="opml_export_success_title">Exportação do OPML realizada com sucesso.</string>
<string name="opml_export_success_sum">O arquivo .opml foi gravado em:\u0020</string>
+ <string name="opml_import_ask_read_permission">Acesso ao armazenamento externo é necessária para ler o arquivo OPML</string>
<!--Sleep timer-->
<string name="set_sleeptimer_label">Configura desligamento automático</string>
<string name="disable_sleeptimer_label">Desabilita desligamento automático</string>
@@ -332,9 +426,24 @@
<string name="sleep_timer_label">Desligamento automático</string>
<string name="time_left_label">Tempo restante:\u0020</string>
<string name="time_dialog_invalid_input">Entrada inválida, a duração precisa ser um número inteiro</string>
+ <string name="timer_about_to_expire_label"><b>Quando o temporizador está prestes a expirar: </b></string>
+ <string name="shake_to_reset_label">Sacudir para reiniciar o temporizador</string>
+ <string name="timer_vibration_label">Vibrar</string>
<string name="time_seconds">segundos</string>
<string name="time_minutes">minutos</string>
<string name="time_hours">horas</string>
+ <plurals name="time_seconds_quantified">
+ <item quantity="one">1 segundo</item>
+ <item quantity="other">%d segundos</item>
+ </plurals>
+ <plurals name="time_minutes_quantified">
+ <item quantity="one">1 minuto</item>
+ <item quantity="other">%d minutos</item>
+ </plurals>
+ <plurals name="time_hours_quantified">
+ <item quantity="one">1 hora</item>
+ <item quantity="other">%d horas</item>
+ </plurals>
<!--gpodder.net-->
<string name="gpodnet_taglist_header">CATEGORIAS</string>
<string name="gpodnet_toplist_header">TOP PODCASTS</string>
@@ -354,6 +463,7 @@
<string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo existente:</string>
<string name="gpodnetauth_device_errorEmpty">ID do dispostivo não pode estar em branco</string>
<string name="gpodnetauth_device_errorAlreadyUsed">ID do dispositivo já está em uso</string>
+ <string name="gpodnetauth_device_caption_errorEmpty">A legenda não deve ser vazia</string>
<string name="gpodnetauth_device_butChoose">Escolher</string>
<string name="gpodnetauth_finish_title">Login realizado com sucesso!</string>
<string name="gpodnetauth_finish_descr">Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net.</string>
@@ -367,29 +477,119 @@
<string name="selected_folder_label">Selecionar pasta:</string>
<string name="create_folder_label">Criar pasta</string>
<string name="choose_data_directory">Escolher pasta de dados</string>
+ <string name="choose_data_directory_message">Por favor escolha a base do seu repositório de dados. O AntennaPode irá criar os sub-diretórios apropriados.</string>
+ <string name="choose_data_directory_permission_rationale">Acesso ao armazenamento externo é necessário para mudar o repositório de dados</string>
<string name="create_folder_msg">Criar nova pasta com o nome \"%1$s\"?</string>
<string name="create_folder_success">Nova pasta criada</string>
<string name="create_folder_error_no_write_access">Não é possível escrever nesta pasta</string>
<string name="create_folder_error_already_exists">Pasta já existente</string>
<string name="create_folder_error">Não foi possível criar pasta</string>
+ <string name="folder_does_not_exist_error">\"%1$s\" não existe</string>
+ <string name="folder_not_readable_error">\"%1$s\" não pode ser lido</string>
+ <string name="folder_not_writable_error">\"%1$s\" não pode ser escrito</string>
<string name="folder_not_empty_dialog_title">A pasta não está vazia</string>
<string name="folder_not_empty_dialog_msg">A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar?</string>
<string name="set_to_default_folder">Escolher pasta padrão</string>
<string name="pref_pausePlaybackForFocusLoss_sum">Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons</string>
<string name="pref_pausePlaybackForFocusLoss_title">Pausar em interrupções</string>
+ <string name="pref_resumeAfterCall_sum">Continuar a reprodução depois que uma ligação telefonica for concluida</string>
+ <string name="pref_resumeAfterCall_title">Continuar após ligação</string>
+ <string name="pref_restart_required">AntennaPode deve ser reiniciado para que esta mudanças tenha efeito.</string>
<!--Online feed view-->
<string name="subscribe_label">Assinar</string>
<string name="subscribed_label">Assinado</string>
+ <string name="downloading_label">Baixando...</string>
<!--Content descriptions for image buttons-->
+ <string name="rewind_label">Voltar</string>
+ <string name="fast_forward_label">Avançar</string>
+ <string name="media_type_audio_label">Áudio</string>
+ <string name="media_type_video_label">Vídeo</string>
+ <string name="navigate_upwards_label">Navegar para cima</string>
+ <string name="status_downloading_label">O epísódio está sendo baixado</string>
<string name="in_queue_label">Episódio está na fila</string>
+ <string name="drag_handle_content_description">Arrastar para mudar a posição deste item</string>
+ <string name="load_next_page_label">Carregar a próxima página</string>
<!--Feed information screen-->
+ <string name="authentication_label">Autenticação</string>
+ <string name="authentication_descr">Mudar o seu usuário e senha para este podcast e seus episódios.</string>
+ <string name="auto_download_settings_label">Configurações de Download Automático</string>
+ <string name="episode_filters_label">Filtrar Episódio</string>
+ <string name="episode_filters_description">Lista de termos utilizados para decidir se um episódio deverá ser incluído ou excluído quando está baixando automaticamente</string>
+ <string name="episode_filters_include">Incluir</string>
+ <string name="episode_filters_exclude">Excluir</string>
+ <string name="episode_filters_hint">Única palavra \n\"Múltiplas palavras\"</string>
+ <string name="keep_updated">Manter Atualizado</string>
<!--Progress information-->
+ <string name="progress_upgrading_database">Atualizar o banco de dados</string>
<!--AntennaPodSP-->
+ <string name="sp_apps_importing_feeds_msg">Importar assinaturas de aplicativos de finalidade única...</string>
+ <string name="search_itunes_label">Procurar no iTunes</string>
+ <string name="filter">Filtrar</string>
<!--Episodes apply actions-->
+ <string name="all_label">Todos</string>
+ <string name="selected_all_label">Selecionar todos Episódios</string>
+ <string name="none_label">Nenhum</string>
+ <string name="deselected_all_label">Desmarcar todos Episódios</string>
+ <string name="played_label">Tocado</string>
+ <string name="selected_played_label">Selecionar episódios tocados</string>
+ <string name="unplayed_label">Não tocado</string>
+ <string name="selected_unplayed_label">Selecionar episódios não tocados</string>
+ <string name="downloaded_label">Baixados</string>
+ <string name="selected_downloaded_label">Selecionar episódios baixados</string>
+ <string name="not_downloaded_label">Não baixado</string>
+ <string name="selected_not_downloaded_label">Selecionar episídios não baixados</string>
+ <string name="queued_label">Enfileirado</string>
+ <string name="selected_queued_label">Selecionar episódios enfileirados</string>
+ <string name="not_queued_label">Não enfileirado</string>
+ <string name="selected_not_queued_label">Selecionar episódios não enfileirados</string>
<!--Sort-->
+ <string name="sort_title_a_z">Título (A \u2192 Z)</string>
+ <string name="sort_title_z_a">Título (Z \u2192 A)</string>
+ <string name="sort_date_new_old">Data (Novo \u2192 Velho)</string>
+ <string name="sort_date_old_new">Data (Velho \u2192 Novo)</string>
+ <string name="sort_duration_short_long">Duração (Curta \u2192 Longa)</string>
+ <string name="sort_duration_long_short">Duração (Longa \u2192 Curta)</string>
<!--Rating dialog-->
+ <string name="rating_title">Gostou do AntennaPod?</string>
+ <string name="rating_message">Nós gostaríamos que você dedicasse um tempo para avaliar o AntennaPod.</string>
+ <string name="rating_never_label">Me deixe em paz</string>
+ <string name="rating_later_label">Lembre-me mais tarde</string>
+ <string name="rating_now_label">Claro, deixe-me fazer isso!</string>
<!--Audio controls-->
+ <string name="audio_controls">Controles de Àudio</string>
+ <string name="playback_speed">Velocidade da Reprodução</string>
+ <string name="volume">Volume</string>
+ <string name="left_short">E</string>
+ <string name="right_short">D</string>
+ <string name="audio_effects">Efeitos Sonoros</string>
+ <string name="stereo_to_mono">Downmix: Stereo para mono</string>
+ <string name="sonic_only">Apenas Sonic</string>
<!--proxy settings-->
+ <string name="proxy_type_label">Tipo</string>
+ <string name="host_label">Host</string>
+ <string name="port_label">Porta</string>
+ <string name="optional_hint">(Opcional)</string>
+ <string name="proxy_test_label">Teste</string>
+ <string name="proxy_checking">Verificando...</string>
+ <string name="proxy_test_successful">Teste realizado com sucesso</string>
+ <string name="proxy_test_failed">Teste falhou</string>
+ <string name="proxy_host_empty_error">Host não pode ser vazio</string>
+ <string name="proxy_host_invalid_error">Host não possui um endereço de IP válido ou dominio</string>
+ <string name="proxy_port_invalid_error">Porta inválida</string>
<!--Casting-->
+ <string name="cast_media_route_menu_title">Tocar em...</string>
+ <string name="cast_disconnect_label">Desconectar a sessão do cast</string>
+ <string name="cast_not_castable">A mídia selecionada não é compatível com o dispositivo cast</string>
+ <string name="cast_failed_to_play">Falha ao iniciar a reprodução da mídia</string>
+ <string name="cast_failed_to_stop">Falha ao parar a reprodução da mídia</string>
+ <string name="cast_failed_to_pause">Falha ao pausar a reprodução da mídia</string>
<!--<string name="cast_failed_to_connect">Could not connect to the device</string>-->
+ <string name="cast_failed_setting_volume">Falha ao configurar o volume</string>
+ <string name="cast_failed_no_connection">Sem conexão com o dispositivo cast</string>
+ <string name="cast_failed_no_connection_trans">A conexão com o dispositivo cast foi perdida. A aplicação está tentando re-estabelecer a coneção, se possivel. Por favor espere por alguns segundos e tente novamente.</string>
+ <string name="cast_failed_perform_action">Falha ao executar a ação</string>
+ <string name="cast_failed_status_request">Falha ao sincronizar com o dispositivo cast</string>
+ <string name="cast_failed_seek">Falha ao buscar uma nova posição no dispositivo cast</string>
+ <string name="cast_failed_receiver_player_error">O receptor de reprodução encontrou um erro no servidor</string>
+ <string name="cast_failed_media_error_skipping">Erro ao tocar a mídia. Pulando...</string>
</resources>
diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml
index 991c185e4..c6ca5285a 100644
--- a/core/src/main/res/values-ru/strings.xml
+++ b/core/src/main/res/values-ru/strings.xml
@@ -1,31 +1,31 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Activitiy and fragment titles-->
- <string name="app_name">AntennaPod</string>
<string name="feeds_label">Каналы</string>
+ <string name="statistics_label">Статистика</string>
<string name="add_feed_label">Добавить подкаст</string>
- <string name="podcasts_label">Подкасты</string>
<string name="episodes_label">Выпуски</string>
- <string name="new_episodes_label">Новые выпуски</string>
- <string name="all_episodes_label">Все выпуски</string>
<string name="all_episodes_short_label">Все</string>
<string name="favorite_episodes_label">Избранное</string>
<string name="new_label">Новые</string>
- <string name="waiting_list_label">В ожидании</string>
<string name="settings_label">Настройки</string>
<string name="add_new_feed_label">Добавить подкаст</string>
<string name="downloads_label">Загрузки</string>
<string name="downloads_running_label">Выполняется</string>
<string name="downloads_completed_label">Завершено</string>
<string name="downloads_log_label">Журнал</string>
+ <string name="subscriptions_label">Подписки</string>
+ <string name="subscriptions_list_label">Перечень подписок</string>
<string name="cancel_download_label">Отменить загрузку</string>
<string name="playback_history_label">Журнал</string>
<string name="gpodnet_main_label">gpodder.net</string>
<string name="gpodnet_auth_label">Войти на gpodder.net</string>
- <!--New episodes fragment-->
- <string name="recently_published_episodes_label">Свежие</string>
- <string name="episode_filter_label">Только новые</string>
+ <string name="free_space_label">свободно %1$s</string>
+ <string name="episode_cache_full_title">Кэш выпусков заполнен</string>
+ <string name="episode_cache_full_message">Достигнут предел кэша выпусков. Объём кэша можно увеличить в Настройках.</string>
<!--Statistics fragment-->
+ <string name="total_time_listened_to_podcasts">Общее время прослушивания подкастов:</string>
+ <string name="statistics_details_dialog">%1$d из %2$d выпусков начато.\n\nПрослушано %3$s из %4$s.</string>
<!--Main activity-->
<string name="drawer_open">Открыть меню</string>
<string name="drawer_close">Закрыть меню</string>
@@ -67,20 +67,28 @@
<string name="length_prefix">Продолжительность:\u0020</string>
<string name="size_prefix">Размер:\u0020</string>
<string name="processing_label">Обработка</string>
+ <string name="loading_label">Загрузка…</string>
<string name="save_username_password_label">Сохранить имя пользователя и пароль</string>
<string name="close_label">Закрыть</string>
<string name="retry_label">Повторить</string>
<string name="auto_download_label">Добавить в автозагрузки</string>
<string name="auto_download_apply_to_items_title">Применить к предыдущим выпускам</string>
<string name="auto_download_apply_to_items_message">Новые настройки <i>Автозагрузки</i> будут автоматически применены к новым выпускам. \nХотите ли вы применить их к ранее опубликованным выпускам?</string>
- <string name="auto_delete_label">Автоматическое удаление выпусков\n(игнорирует общие настройки)</string>
+ <string name="auto_delete_label">Автоматически удалить выпуск</string>
<string name="parallel_downloads_suffix">\u0020одновременных загрузок</string>
- <string name="feed_auto_download_global">Общие</string>
+ <string name="feed_auto_download_global">По умолчанию для всех</string>
<string name="feed_auto_download_always">Всегда</string>
<string name="feed_auto_download_never">Никогда</string>
+ <string name="send_label">Отправить…</string>
<string name="episode_cleanup_never">Никогда</string>
<string name="episode_cleanup_queue_removal">Когда не в очереди</string>
<string name="episode_cleanup_after_listening">После прослушивания</string>
+ <plurals name="episode_cleanup_days_after_listening">
+ <item quantity="one">день после прослушивания</item>
+ <item quantity="few">%d дня после прослушивания</item>
+ <item quantity="many">%d дней после прослушивания</item>
+ <item quantity="other">%d дней после прослушивания</item>
+ </plurals>
<!--'Add Feed' Activity labels-->
<string name="feedurl_label">URL канала</string>
<string name="etxtFeedurlHint">www.example.com/feed</string>
@@ -93,18 +101,19 @@
<string name="mark_all_read_msg">Отметить все выпуски как прослушанные</string>
<string name="mark_all_read_confirmation_msg">Подтвердите, что хотите пометить все эпизоды как прослушанные.</string>
<string name="mark_all_read_feed_confirmation_msg">Подтвердите, что хотите пометить все эпизоды в этом канале как прослушанные.</string>
- <string name="mark_all_seen_label">Отметить все как про</string>
+ <string name="mark_all_seen_label">Отметить все как замеченное</string>
<string name="show_info_label">Показать информацию</string>
<string name="remove_feed_label">Удалить подкаст</string>
+ <string name="share_label">Поделиться…</string>
<string name="share_link_label">Поделиться ссылкой</string>
<string name="share_link_with_position_label">Поделиться ссылкой с отметкой времени</string>
<string name="share_feed_url_label">Поделиться ссылкой на канал</string>
- <string name="share_item_url_label">Поделиться ссылкой на выпуск</string>
- <string name="share_item_url_with_position_label">Поделиться ссылкой с отметкой времени</string>
+ <string name="share_item_url_label">Поделиться ссылкой на файл выпуска</string>
+ <string name="share_item_url_with_position_label">Поделиться ссылкой на файл выпуска с отметкой времени</string>
<string name="feed_delete_confirmation_msg">Подтвердите удаление канала и всех выпусков, загруженных с этого канала.</string>
<string name="feed_remover_msg">Удаление канала</string>
<string name="load_complete_feed">Обновить весь канал</string>
- <string name="hide_episodes_title">Скрыть выпуск</string>
+ <string name="hide_episodes_title">Скрыть выпуски</string>
<string name="episode_actions">Применить действия</string>
<string name="hide_unplayed_episodes_label">Непрослушанное</string>
<string name="hide_paused_episodes_label">Приостановленное</string>
@@ -113,7 +122,9 @@
<string name="hide_not_queued_episodes_label">Не в очереди</string>
<string name="hide_downloaded_episodes_label">Загружено</string>
<string name="hide_not_downloaded_episodes_label">Не загружено</string>
+ <string name="filtered_label">Отфильтровано</string>
<string name="refresh_failed_msg">{fa-exclamation-circle} Последнее обновление не удалось</string>
+ <string name="open_podcast">Открыть подкаст</string>
<!--actions on feeditems-->
<string name="download_label">Загрузить</string>
<string name="play_label">Воспроизвести</string>
@@ -129,14 +140,14 @@
<string name="added_to_queue_label">Добавлено в очередь</string>
<string name="remove_from_queue_label">Удалить из очереди</string>
<string name="add_to_favorite_label">Добавить в избранное</string>
+ <string name="added_to_favorites">Добавлено в избранное</string>
<string name="remove_from_favorite_label">Удалить из избранного</string>
+ <string name="removed_from_favorites">Удалено из избранного</string>
<string name="visit_website_label">Посетить сайт</string>
<string name="support_label">Поддержать через Flattr</string>
- <string name="enqueue_all_new">Добавить всё в очередь</string>
- <string name="download_all">Загрузить всё</string>
<string name="skip_episode_label">Пропустить выпуск</string>
<string name="activate_auto_download">Включить автоматическую загрузку</string>
- <string name="deactivate_auto_download">Выключить автоматическую загрузку</string>
+ <string name="deactivate_auto_download">Отключить автоматическую загрузку</string>
<string name="reset_position">Сбросить время воспроизведения</string>
<string name="removed_item">Удалено</string>
<!--Download messages and labels-->
@@ -154,15 +165,23 @@
<string name="download_error_connection_error">Ошибка соединения</string>
<string name="download_error_unknown_host">Неизвестный узел</string>
<string name="download_error_unauthorized">Ошибка авторизации</string>
+ <string name="download_error_file_type_type">Ошибка в типе файла</string>
+ <string name="download_error_forbidden">Доступ запрещён</string>
<string name="cancel_all_downloads_label">Отменить все загрузки</string>
<string name="download_canceled_msg">Загрузка отменена</string>
<string name="download_canceled_autodownload_enabled_msg">Загрузка отменена\nОтключена <i>Автоматическая загрузка</i> для этого эпизода</string>
- <string name="download_report_title">Загрузки завершены</string>
- <string name="download_report_content_title">Отчет о загрузках</string>
+ <string name="download_report_title">Загрузки завершились с ошибкой</string>
+ <string name="download_report_content_title">Отчёт о загрузках</string>
<string name="download_error_malformed_url">Неправильный адрес</string>
<string name="download_error_io_error">Ошибка ввода-вывода</string>
<string name="download_error_request_error">Ошибка запроса</string>
<string name="download_error_db_access">Ошибка доступа к базе данных</string>
+ <plurals name="downloads_left">
+ <item quantity="one">осталась %d загрузка</item>
+ <item quantity="few">осталось %d загрузки</item>
+ <item quantity="many">осталось %d загрузок</item>
+ <item quantity="other">осталось %d загрузок</item>
+ </plurals>
<string name="downloads_processing">Производится загрузка</string>
<string name="download_notification_title">Получение данных подкаста</string>
<string name="download_report_content">%1$d загрузок завершено, %2$d не удалось</string>
@@ -187,13 +206,14 @@
<string name="playback_error_server_died">Сервер недоступен</string>
<string name="playback_error_unknown">Неизвестная ошибка</string>
<string name="no_media_playing_label">Ничего не воспроизводится</string>
- <string name="position_default_label">00:00:00</string>
<string name="player_buffering_msg">Буферизация</string>
<string name="playbackservice_notification_title">Воспроизведение подкаста</string>
<string name="unknown_media_key">AntennaPod - неизвестный ключ носителя: %1$d</string>
<!--Queue operations-->
<string name="lock_queue">Заблокировать очередь</string>
<string name="unlock_queue">Разблокировать очередь</string>
+ <string name="queue_locked">Очередь заблокирована</string>
+ <string name="queue_unlocked">Очередь разблокирована</string>
<string name="clear_queue_label">Очистить очередь</string>
<string name="undo">Отмена</string>
<string name="removed_from_queue">Удалено</string>
@@ -235,21 +255,29 @@
<!--Variable Speed-->
<string name="download_plugin_label">Загрузить плагин</string>
<string name="no_playback_plugin_title">Плагин не установлен</string>
+ <string name="no_playback_plugin_or_sonic_msg">Чтобы воспользоваться изменением скорости воспроизведения, рекомендуется включить встроенный медиапроигрыватель Sonic [Android 4.1+].\n\nИли же можно загрузить сторонний плагин <i>Prestissimo</i> из Play Store.\nОбо всех проблемах, относящихся к Prestissimo стоит сообщать владельцу плагина, AntennaPod к этому не причастен.</string>
<string name="set_playback_speed_label">Скорость воспроизведения</string>
<string name="enable_sonic">Включить Sonic</string>
<!--Empty list labels-->
<string name="no_items_label">Список пуст</string>
<string name="no_feeds_label">Вы еще не подписаны ни на один канал</string>
+ <string name="no_chapters_label">Этот выпуск не содержит оглавления.</string>
+ <string name="no_shownotes_label">Этот выпуск не содержит примечаний.</string>
<!--Preferences-->
+ <string name="storage_pref">Хранилище</string>
+ <string name="project_pref">Проект</string>
<string name="other_pref">Прочее</string>
<string name="about_pref">О программе</string>
<string name="queue_label">Очередь</string>
<string name="services_label">Сервисы</string>
<string name="flattr_label">Flattr</string>
<string name="pref_episode_cleanup_title">Удаление выпусков</string>
+ <string name="pref_episode_cleanup_summary">Выпуски, которые не стоят в очереди и не отмечены как избранные могут быть удалены для освобождения места под Автозагрузку.</string>
<string name="pref_pauseOnDisconnect_sum">Приостановить воспроизведение, когда наушники или bluetooth отключены</string>
<string name="pref_unpauseOnHeadsetReconnect_sum">Продолжать воспроизведение после подключения наушников</string>
<string name="pref_unpauseOnBluetoothReconnect_sum">Продолжать воспроизведение после подключения наушников или восстановления bluetooth-соединения</string>
+ <string name="pref_hardwareForwardButtonSkips_title">Пропускать кнопкой перемотки вперёд</string>
+ <string name="pref_hardwareForwardButtonSkips_sum">При нажатии на физическую кнопку перемотки вперёд переходить к следующему выпуску вместо перемотки</string>
<string name="pref_followQueue_sum">После завершения воспроизведения перейти к следующему в очереди</string>
<string name="pref_auto_delete_sum">Удалять эпизод после завершения воспроизведения</string>
<string name="pref_auto_delete_title">Автоматическое удаление</string>
@@ -265,6 +293,8 @@
<string name="pref_autoUpdateIntervallOrTime_Disable">Отключить</string>
<string name="pref_autoUpdateIntervallOrTime_Interval">Задать интервал</string>
<string name="pref_autoUpdateIntervallOrTime_TimeOfDay">Задать время</string>
+ <string name="pref_autoUpdateIntervallOrTime_every">каждые %1$s</string>
+ <string name="pref_autoUpdateIntervallOrTime_at">в %1$s</string>
<string name="pref_downloadMediaOnWifiOnly_sum">Загружать файлы только через Wi-Fi</string>
<string name="pref_followQueue_title">Непрерывное воспроизведение</string>
<string name="pref_downloadMediaOnWifiOnly_title">Загрузка по Wi-Fi</string>
@@ -314,6 +344,10 @@
<string name="pref_gpodnet_logout_toast">Выход произведён успешно</string>
<string name="pref_gpodnet_setlogin_information_title">Изменить информацию авторизации</string>
<string name="pref_gpodnet_setlogin_information_sum">Изменить информацию авторизации для аккаунта gpodder.net</string>
+ <string name="pref_gpodnet_sync_title">Синхронизировать</string>
+ <string name="pref_gpodnet_sync_sum">Синхронизировать подписки и статус выпусков с сервисом gpodder.net</string>
+ <string name="pref_gpodnet_sync_started">Синхронизация запущена</string>
+ <string name="pref_gpodnet_login_status"><![CDATA[Вход как <i>%1$s</i> с устройства <i>%2$s</i>]]></string>
<string name="pref_playback_speed_title">Скорость воспроизведения</string>
<string name="pref_playback_speed_sum">Настроить скорости воспроизведения</string>
<string name="pref_fast_forward">Интервал быстрой перемотки вперед</string>
@@ -324,9 +358,15 @@
<string name="pref_expandNotify_sum">Всегда показывать в уведомлении кнопки управления вопспроизведением</string>
<string name="pref_persistNotify_title">Постоянный контрооль воспроизведения</string>
<string name="pref_persistNotify_sum">Сохранять уведомление и кнопки воспроизведения на экране блокировки во время паузы.</string>
+ <string name="pref_compact_notification_buttons_title">Выбрать кнопки экрана блокировки</string>
+ <string name="pref_compact_notification_buttons_sum">Поменять кнопки управления на экране блокировки. Кнопка воспроизведения/паузы присутствует постоянно.</string>
+ <string name="pref_compact_notification_buttons_dialog_title">Выберите не более %1$d элементов</string>
+ <string name="pref_compact_notification_buttons_dialog_error">Нельзя выбрать больше %1$d элементов.</string>
+ <string name="pref_show_subscriptions_in_drawer_title">Показать подписки</string>
+ <string name="pref_show_subscriptions_in_drawer_sum">Показывать перечень подписок в боковой панели</string>
<string name="pref_lockscreen_background_title">Выбрать фон экрана блокировки</string>
<string name="pref_lockscreen_background_sum">Изменяет фон экрана блокировки на обложку выпуска. Кроме того показывает обложку в сторонних приложениях.</string>
- <string name="pref_showDownloadReport_title">Показывать отчет о загрузках</string>
+ <string name="pref_showDownloadReport_title">Показывать отчёт о загрузках</string>
<string name="pref_showDownloadReport_sum">Если загрузка не удается, показывать отчет с подробностями об ошибке.</string>
<string name="pref_expand_notify_unsupport_toast">Версии Android ниже 4.1 не поддерживают расширенные уведомления.</string>
<string name="pref_queueAddToFront_sum">Добавлять новые выпуски в начало очереди.</string>
@@ -334,10 +374,20 @@
<string name="pref_smart_mark_as_played_disabled">Отключено</string>
<string name="pref_image_cache_size_title">Размер кэша для изображений</string>
<string name="pref_image_cache_size_sum">Размер дискового кэша для изображений</string>
- <string name="crash_report_title">Отчет о сбое</string>
+ <string name="crash_report_title">Отчёт о сбое</string>
+ <string name="crash_report_sum">Отослать последний отчёт о сбое по e-mail</string>
<string name="send_email">Отправить Email</string>
<string name="experimental_pref">Экспериментальные настройки</string>
<string name="pref_sonic_title">Проигрыватель Sonic</string>
+ <string name="pref_sonic_message">Задействовать встроенный медиа проигрыватель Sonic вместо стандартного из ОС Android и Prestissimo</string>
+ <string name="pref_current_value">Текущее значение: %1$s</string>
+ <string name="pref_proxy_title">Прокси</string>
+ <string name="pref_proxy_sum">Настройки прокси</string>
+ <string name="pref_faq">ЧаВо</string>
+ <string name="pref_known_issues">Известные проблемы</string>
+ <string name="pref_no_browser_found">Веб-браузер не обнаружен.</string>
+ <string name="pref_cast_title">Поддержка Chromecast</string>
+ <string name="pref_cast_message">Включить поддержку удалённого воспроизведения на устройствах с Google Cast (таких как Chromecast, динамики или ТВ на ОС Android)</string>
<!--Auto-Flattr dialog-->
<string name="auto_flattr_enable">Включить автоматическую поддержку через Flattr</string>
<string name="auto_flattr_after_percent">Поддерживать через Flattr эпизоды, прослушанные на %d процентов</string>
@@ -360,14 +410,19 @@
<string name="opml_import_label">Импорт OPML</string>
<string name="opml_directory_error">Ошибка</string>
<string name="reading_opml_label">Чтение файла OPML</string>
+ <string name="opml_reader_error">При чтении OPML произошла ошибка:</string>
+ <string name="opml_import_error_no_file">Файл не выбран!</string>
<string name="select_all_label">Отметить все</string>
<string name="deselect_all_label">Снять все отметки</string>
+ <string name="select_options_label">Отметить…</string>
<string name="choose_file_from_filesystem">Из файловой системы</string>
<string name="choose_file_from_external_application">С помощью внешнего приложения</string>
<string name="opml_export_label">Экспорт в OPML</string>
+ <string name="exporting_label">Экспортируется...</string>
<string name="export_error_label">Ошибка экспорта</string>
<string name="opml_export_success_title">OPML успешно экспортирован.</string>
<string name="opml_export_success_sum">Файл OPML был записан в:\u0020</string>
+ <string name="opml_import_ask_read_permission">Для чтения файла OPML необходим доступ к внешнему хранилищу</string>
<!--Sleep timer-->
<string name="set_sleeptimer_label">Установить таймер сна</string>
<string name="disable_sleeptimer_label">Отключить таймер сна</string>
@@ -377,9 +432,9 @@
<string name="time_dialog_invalid_input">Неправильный ввод, время должно быть в виде числа</string>
<string name="timer_about_to_expire_label"><b>Когда истекает таймер:</b></string>
<string name="shake_to_reset_label">Потрясти для сброса таймера</string>
- <string name="timer_vibration_label">Вибрация</string>
- <string name="time_seconds">с</string>
- <string name="time_minutes">м</string>
+ <string name="timer_vibration_label">Вибрировать</string>
+ <string name="time_seconds">сек</string>
+ <string name="time_minutes">мин</string>
<string name="time_hours">ч</string>
<plurals name="time_seconds_quantified">
<item quantity="one">1 секунда</item>
@@ -418,6 +473,7 @@
<string name="gpodnetauth_device_chooseExistingDevice">Выберите существующее устройство:</string>
<string name="gpodnetauth_device_errorEmpty">Поле с Device ID не должно быть пустым</string>
<string name="gpodnetauth_device_errorAlreadyUsed">Device ID уже используется</string>
+ <string name="gpodnetauth_device_caption_errorEmpty">Обязательно заполнить</string>
<string name="gpodnetauth_device_butChoose">Выберите</string>
<string name="gpodnetauth_finish_title">Авторизация успешна!</string>
<string name="gpodnetauth_finish_descr">Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net</string>
@@ -431,6 +487,8 @@
<string name="selected_folder_label">Выбранная папка:</string>
<string name="create_folder_label">Создать папку</string>
<string name="choose_data_directory">Выбрать папку для хранения данных</string>
+ <string name="choose_data_directory_message">Укажите корневую папку для хранения данных. AntennaPod создаст необходимые подкаталоги.</string>
+ <string name="choose_data_directory_permission_rationale">Для смены папки хранения данных необходим доступ к внешнему хранилищу</string>
<string name="create_folder_msg">Создать папку \"%1$s\"?</string>
<string name="create_folder_success">Новая папка создана</string>
<string name="create_folder_error_no_write_access">Запись в эту папку невозможна</string>
@@ -450,33 +508,34 @@
<!--Online feed view-->
<string name="subscribe_label">Подписаться</string>
<string name="subscribed_label">Подписка оформлена</string>
+ <string name="downloading_label">Идёт загрузка…</string>
<!--Content descriptions for image buttons-->
- <string name="show_chapters_label">Показать главы</string>
- <string name="show_shownotes_label">Показать примечания к выпуску</string>
- <string name="show_cover_label">Показать изображение</string>
<string name="rewind_label">Назад</string>
<string name="fast_forward_label">Вперед</string>
<string name="media_type_audio_label">Аудио</string>
<string name="media_type_video_label">Видео</string>
<string name="navigate_upwards_label">Перейти выше</string>
- <string name="butAction_label">Другие действия</string>
- <string name="status_playing_label">Выпуск воспроизводится</string>
<string name="status_downloading_label">Выпуск загружается</string>
- <string name="status_downloaded_label">Выпуск загружен</string>
- <string name="status_unread_label">Новый элемент</string>
<string name="in_queue_label">Выпуск в очереди</string>
- <string name="new_episodes_count_label">Количество новых выпусков</string>
- <string name="in_progress_episodes_count_label">Количество начатых выпусков</string>
<string name="drag_handle_content_description">Перетяните чтобы изменить позицию этого элемента</string>
<string name="load_next_page_label">Загрузить следующую страницу</string>
<!--Feed information screen-->
<string name="authentication_label">Авторизация</string>
<string name="authentication_descr">Изменить имя пользователя и пароль для этого подкаста и его выпусков.</string>
+ <string name="auto_download_settings_label">Настройки автозагрузки</string>
+ <string name="episode_filters_label">Фильтр выпусков</string>
+ <string name="episode_filters_description">Перечень условий по включению или исключению выпуска из списков автоматической загрузки</string>
+ <string name="episode_filters_include">Включить</string>
+ <string name="episode_filters_exclude">Исключить</string>
+ <string name="episode_filters_hint">По одному слову \n\"По фразе\"</string>
+ <string name="keep_updated">Постоянно обновлять</string>
<!--Progress information-->
<string name="progress_upgrading_database">Обновление базы данных</string>
<!--AntennaPodSP-->
<string name="sp_apps_importing_feeds_msg">Импорт подписок из одноцелевых приложений…</string>
<string name="search_itunes_label">Поиск в iTunes</string>
+ <string name="filter">Фильтровать</string>
+ <!--Episodes apply actions-->
<string name="all_label">Все</string>
<string name="selected_all_label">Отмечены все выпуски</string>
<string name="none_label">Нет</string>
@@ -489,6 +548,11 @@
<string name="selected_downloaded_label">Выбраны загруженные выпуски</string>
<string name="not_downloaded_label">Не загружено</string>
<string name="selected_not_downloaded_label">Выбраны незагруженные выпуски</string>
+ <string name="queued_label">В очереди</string>
+ <string name="selected_queued_label">Выбраны выпуски из очереди</string>
+ <string name="not_queued_label">Не в очереди</string>
+ <string name="selected_not_queued_label">Выбраны выпуски не из очереди</string>
+ <!--Sort-->
<string name="sort_title_a_z">Заголовку (А \u2192 Я)</string>
<string name="sort_title_z_a">Заголовку (Я \u2192 А)</string>
<string name="sort_date_new_old">Дате (Новое \u2192 Старое)</string>
@@ -502,5 +566,40 @@
<string name="rating_later_label">Напомни позже</string>
<string name="rating_now_label">Конечно, давай!</string>
<!--Audio controls-->
+ <string name="audio_controls">Управление звучанием</string>
+ <string name="playback_speed">Скорость воспроизведения</string>
+ <string name="volume">Громкость</string>
+ <string name="left_short">Л</string>
+ <string name="right_short">П</string>
+ <string name="audio_effects">Звуковые эффекты</string>
+ <string name="stereo_to_mono">Низвести стерео до моно</string>
+ <string name="sonic_only">Только для Sonic</string>
<!--proxy settings-->
+ <string name="proxy_type_label">Тип</string>
+ <string name="host_label">Хост</string>
+ <string name="port_label">Порт</string>
+ <string name="optional_hint">(необязательно)</string>
+ <string name="proxy_test_label">Проверить</string>
+ <string name="proxy_checking">Проверка…</string>
+ <string name="proxy_test_successful">Проверка пройдена</string>
+ <string name="proxy_test_failed">Проверка не пройдена</string>
+ <string name="proxy_host_empty_error">Обязательно укажите хост</string>
+ <string name="proxy_host_invalid_error">Неверный адрес или домен хоста</string>
+ <string name="proxy_port_invalid_error">Неверный порт</string>
+ <!--Casting-->
+ <string name="cast_media_route_menu_title">Воспроизвести на…</string>
+ <string name="cast_disconnect_label">Закрыть сеанс передачи Google cast</string>
+ <string name="cast_not_castable">Выбранный формат не поддерживается этим устройством Google cast</string>
+ <string name="cast_failed_to_play">Невозможно начать воспроизведение</string>
+ <string name="cast_failed_to_stop">Невозможно остановить воспроизведение</string>
+ <string name="cast_failed_to_pause">Невозможно приостановить воспроизведение</string>
+ <!--<string name="cast_failed_to_connect">Could not connect to the device</string>-->
+ <string name="cast_failed_setting_volume">Невозможно установить громкость</string>
+ <string name="cast_failed_no_connection">Нет соединений с устройствами Google cast</string>
+ <string name="cast_failed_no_connection_trans">Соединение с устройством Google cast потеряно. Идёт попытка восстановления соединения. Пожалуйста, подождите пару секунд и попробуйте снова.</string>
+ <string name="cast_failed_perform_action">Не удалось выполнить действие</string>
+ <string name="cast_failed_status_request">Не удалось синхронизироваться с устройством Google cast</string>
+ <string name="cast_failed_seek">Не удалось выполнить перемотку на устройстве Google cast</string>
+ <string name="cast_failed_receiver_player_error">Серьёзная ошибка воспроизведения в устройстве Google cast</string>
+ <string name="cast_failed_media_error_skipping">Ошибка воспроизведения. Пропускаю…</string>
</resources>
diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml
index 168945c28..43707e8f4 100644
--- a/core/src/main/res/values-uk-rUA/strings.xml
+++ b/core/src/main/res/values-uk-rUA/strings.xml
@@ -1,24 +1,21 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Activitiy and fragment titles-->
- <string name="app_name">AntennaPod</string>
<string name="feeds_label">Канали</string>
<string name="statistics_label">Статистика</string>
<string name="add_feed_label">Додати подкаст</string>
- <string name="podcasts_label">Подкасти</string>
<string name="episodes_label">Епізоди</string>
- <string name="new_episodes_label">Нові епізоди</string>
- <string name="all_episodes_label">Всі епізоди</string>
<string name="all_episodes_short_label">Всі</string>
<string name="favorite_episodes_label">Улюблені</string>
<string name="new_label">Нові</string>
- <string name="waiting_list_label">Черга</string>
<string name="settings_label">Налаштування</string>
<string name="add_new_feed_label">Додати подкаст</string>
<string name="downloads_label">Завантаження</string>
<string name="downloads_running_label">В процесі</string>
<string name="downloads_completed_label">Завершено</string>
<string name="downloads_log_label">Журнал</string>
+ <string name="subscriptions_label">Підписки</string>
+ <string name="subscriptions_list_label">Перелік підписок</string>
<string name="cancel_download_label">Скасувати завантаження</string>
<string name="playback_history_label">Що грало</string>
<string name="gpodnet_main_label">gpodder.net</string>
@@ -26,9 +23,6 @@
<string name="free_space_label">%1$s вільно</string>
<string name="episode_cache_full_title">Кеш епізодів заповнений</string>
<string name="episode_cache_full_message">Досягнута межа розміра кеша епізодів. Розмір кеша можна збільшити в налаштуваннях.</string>
- <!--New episodes fragment-->
- <string name="recently_published_episodes_label">Щойно опубліковано</string>
- <string name="episode_filter_label">Показати тількі нові епізоди</string>
<!--Statistics fragment-->
<string name="total_time_listened_to_podcasts">Загальний час прослуханих подкастів:</string>
<string name="statistics_details_dialog">%1$d з %2$d епізодів почато.\n\nПрослухано %3$s з %4$s.</string>
@@ -80,9 +74,9 @@
<string name="auto_download_label">Включити до автозавантаження</string>
<string name="auto_download_apply_to_items_title">Застосувати до попередніх епізодів</string>
<string name="auto_download_apply_to_items_message">Нове налаштування <i>Автозагрузка</i> буде автоматично застосоване до нових епізодів.\nБажаєте також застосувати його до тих епізодів що було опубліковано раніше?</string>
- <string name="auto_delete_label">Автоматичне видалення епізода\n(перевизначити глобальне налаштування за замовчуванням)</string>
+ <string name="auto_delete_label">Автовидалення епізода</string>
<string name="parallel_downloads_suffix">\u0020паралельних завантажень</string>
- <string name="feed_auto_download_global">Для всіх</string>
+ <string name="feed_auto_download_global">За замовчанням</string>
<string name="feed_auto_download_always">Завжди</string>
<string name="feed_auto_download_never">Ніколи</string>
<string name="send_label">Відправити…</string>
@@ -113,8 +107,8 @@
<string name="share_link_label">Поділитися URL сайту</string>
<string name="share_link_with_position_label">Поділитись посиланням на позицію</string>
<string name="share_feed_url_label">Поділитись посиланням на канал</string>
- <string name="share_item_url_label">Поділитись посиланням на епізод</string>
- <string name="share_item_url_with_position_label">Поділитись посиланням на епізод з позицією</string>
+ <string name="share_item_url_label">Поділитись посиланням на файл епізода</string>
+ <string name="share_item_url_with_position_label">Поділитись посиланням на файл епізода з позицією</string>
<string name="feed_delete_confirmation_msg">Ви впенені що хочете видаліти канал та всі завантажені епізоди?</string>
<string name="feed_remover_msg">Удаляю канал</string>
<string name="load_complete_feed">Оновити канал цілком</string>
@@ -150,8 +144,6 @@
<string name="removed_from_favorites">Видалено з улюблених</string>
<string name="visit_website_label">Відкрити сайт</string>
<string name="support_label">Підтримати за допомогою Flattr</string>
- <string name="enqueue_all_new">Додати до черги</string>
- <string name="download_all">Завантажити все</string>
<string name="skip_episode_label">Пропустити епізод</string>
<string name="activate_auto_download">Включити автозавантаження</string>
<string name="deactivate_auto_download">Виключити автозавантаження</string>
@@ -173,6 +165,7 @@
<string name="download_error_unknown_host">Невідомий host</string>
<string name="download_error_unauthorized">Помилка автентифікації</string>
<string name="download_error_file_type_type">Помилка типа файла</string>
+ <string name="download_error_forbidden">Заборонено</string>
<string name="cancel_all_downloads_label">Скасувати всі завантаження</string>
<string name="download_canceled_msg">Завантаження скасоване</string>
<string name="download_canceled_autodownload_enabled_msg">Завантаження скасоване\n<i>Автозавантаження</i> для цього елемента вимкнуто</string>
@@ -211,7 +204,6 @@
<string name="playback_error_server_died">Сервер помер</string>
<string name="playback_error_unknown">Невідома помилка</string>
<string name="no_media_playing_label">Немає що грати</string>
- <string name="position_default_label">00:00:00</string>
<string name="player_buffering_msg">Буферізую</string>
<string name="playbackservice_notification_title">Грає подкаст</string>
<string name="unknown_media_key">AntennaPod - Невідомий медіа ключ: %1$d</string>
@@ -270,6 +262,8 @@
<string name="no_chapters_label">В цьому епізоді немає розділів.</string>
<string name="no_shownotes_label">До цього епізода немає нотаток.</string>
<!--Preferences-->
+ <string name="storage_pref">Зберігання</string>
+ <string name="project_pref">Проект</string>
<string name="other_pref">Інше</string>
<string name="about_pref">Про програму</string>
<string name="queue_label">Черга</string>
@@ -362,6 +356,12 @@
<string name="pref_expandNotify_sum">Завжди розгортати повідомлення, щоб показати кнопки керування.</string>
<string name="pref_persistNotify_title">Завжди показувати елементи керування відтворенням</string>
<string name="pref_persistNotify_sum">Показувати повідомлення та елементи керування на екрані блокування в режимі паузи.</string>
+ <string name="pref_compact_notification_buttons_title">Налаштувати кнопки на екрані блокування</string>
+ <string name="pref_compact_notification_buttons_sum">Змінити кнопки на екрані блокування. Кнопки програвання/пауза завжди включені.</string>
+ <string name="pref_compact_notification_buttons_dialog_title">Оберіть не більше ніж %1$d кнопок</string>
+ <string name="pref_compact_notification_buttons_dialog_error">Ви можете обрати не більше ніж %1$d кнопок.</string>
+ <string name="pref_show_subscriptions_in_drawer_title">Показати підписки</string>
+ <string name="pref_show_subscriptions_in_drawer_sum">Показати перелік підписок безпосередньо в меню навігації</string>
<string name="pref_lockscreen_background_title">Змінювати фон екрана блокування</string>
<string name="pref_lockscreen_background_sum">Встановити картинку поточного епізода як фон екрана блокування. Побічний ефект - це зображення буде також видно в інших додатках.</string>
<string name="pref_showDownloadReport_title">Показати звіт про завантаження</string>
@@ -381,8 +381,10 @@
<string name="pref_current_value">Поточне значення: %1$s</string>
<string name="pref_proxy_title">Проксі</string>
<string name="pref_proxy_sum">Застосувати проксі сервер</string>
+ <string name="pref_faq">ЧаПи</string>
<string name="pref_known_issues">Відомі проблеми</string>
- <string name="pref_no_browser_found">Веб-браузер не знайдено.\"</string>
+ <string name="pref_no_browser_found">Веб браузер не знайдено.</string>
+ <string name="pref_cast_title">Підтримка для Chromecast</string>
<!--Auto-Flattr dialog-->
<string name="auto_flattr_enable">Включити автоматичне заохочення авторів через сервіс flattr</string>
<string name="auto_flattr_after_percent">Заохотити автора через Flattr щойно %d відсотків епізода було відтворено</string>
@@ -501,22 +503,13 @@
<string name="subscribed_label">Підписано</string>
<string name="downloading_label">Завантажується…</string>
<!--Content descriptions for image buttons-->
- <string name="show_chapters_label">Показати глави</string>
- <string name="show_shownotes_label">Показати нотатки</string>
- <string name="show_cover_label">Показати зображення</string>
<string name="rewind_label">Перемотка назад</string>
<string name="fast_forward_label">Перемотка вперед</string>
<string name="media_type_audio_label">Звук</string>
<string name="media_type_video_label">Відео</string>
<string name="navigate_upwards_label">Догори</string>
- <string name="butAction_label">Додаткові дії</string>
- <string name="status_playing_label">Епізод програється</string>
<string name="status_downloading_label">Епізод завантажується</string>
- <string name="status_downloaded_label">Епізод завантажено</string>
- <string name="status_unread_label">Нове</string>
<string name="in_queue_label">Епізод чекає в черзі</string>
- <string name="new_episodes_count_label">Кількість нових епізодів</string>
- <string name="in_progress_episodes_count_label">Кількість епізодів що ви почали слухати</string>
<string name="drag_handle_content_description">Перетягніть щоб змінити позицію</string>
<string name="load_next_page_label">Завантажити наступну сторінку</string>
<!--Feed information screen-->
@@ -534,8 +527,8 @@
<!--AntennaPodSP-->
<string name="sp_apps_importing_feeds_msg">Імпорт подкастів з інших програм...</string>
<string name="search_itunes_label">Пошук в iTunes</string>
- <string name="select_label"><b>Обрати…</b></string>
<string name="filter">Фільтр</string>
+ <!--Episodes apply actions-->
<string name="all_label">Всі</string>
<string name="selected_all_label">Обрано всі епізоди</string>
<string name="none_label">Жодного</string>
@@ -552,7 +545,7 @@
<string name="selected_queued_label">Обрано епізоди що в черзі</string>
<string name="not_queued_label">Не в черзі</string>
<string name="selected_not_queued_label">Обрано епізоди що не в черзі</string>
- <string name="sort_title"><b>Сортувати за…</b></string>
+ <!--Sort-->
<string name="sort_title_a_z">Назва (А \u2192 Я)</string>
<string name="sort_title_z_a">Назва (Я \u2192 А)</string>
<string name="sort_date_new_old">Дата (Нові \u2192 Старі)</string>
@@ -583,7 +576,7 @@
<string name="proxy_checking">Перевіряється...</string>
<string name="proxy_test_successful">Протестовано успішно</string>
<string name="proxy_test_failed">Протестовано з помилками</string>
- <string name="proxy_host_empty_error">Хост не можете бути пустим</string>
- <string name="proxy_host_invalid_error">Хост не є правильною IP-адресою або доменним ім\'ям</string>
<string name="proxy_port_invalid_error">Неправильний порт</string>
+ <!--Casting-->
+ <!--<string name="cast_failed_to_connect">Could not connect to the device</string>-->
</resources>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 620bc2b65..f4ed79937 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -4,7 +4,6 @@
tools:ignore="MissingTranslation">
<!-- Activitiy and fragment titles -->
- <string name="app_name" translate="false">AntennaPod</string>
<string name="feeds_label">Feeds</string>
<string name="statistics_label">Statistics</string>
<string name="add_feed_label">Add Podcast</string>
@@ -233,9 +232,10 @@
<string name="move_to_top_label">Move to top</string>
<string name="move_to_bottom_label">Move to bottom</string>
<string name="sort">Sort</string>
- <string name="alpha">Alphabetically</string>
<string name="date">Date</string>
<string name="duration">Duration</string>
+ <string name="episode_title">Episode title</string>
+ <string name="feed_title">Feed title</string>
<string name="ascending">Ascending</string>
<string name="descending">Descending</string>
<string name="clear_queue_confirmation_msg">Please confirm that you want to clear the queue of ALL of the episodes in it</string>
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
index 9bbccbb82..9bbccbb82 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
index 213dd1875..213dd1875 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
index 5b1fdab61..5b1fdab61 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
index f0a7214c9..f0a7214c9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
index fe4183d54..fe4183d54 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java b/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java
index e2d8f8ad5..e2d8f8ad5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
index f063cf5e3..f063cf5e3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/play/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index 068669af9..068669af9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/feed/FeedMedia.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index e2d63a385..e2d63a385 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
index 4262b8a70..4262b8a70 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/play/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 201efbc81..201efbc81 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/util/playback/Playable.java