diff options
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 |