Merge branch 'develop' into exo-player

This commit is contained in:
H. Lehmann 2018-05-26 23:45:18 +02:00 committed by GitHub
commit 532d822b3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1573 additions and 963 deletions

36
.circleci/config.yml Normal file
View File

@ -0,0 +1,36 @@
version: 2
jobs:
build:
docker:
- image: circleci/android:api-26-alpha
working_directory: ~/AntennaPod
environment:
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"'
_JAVA_OPTIONS: "-Xms256m -Xmx1280m"
steps:
- checkout
- restore_cache:
keys:
- v1-android-{{ checksum "build.gradle" }}
# fallback to using the latest cache if no exact match is found
- v1-android-
- run:
command: ./gradlew assembleDebug -PdisablePreDex
no_output_timeout: 1800
- store_artifacts:
path: app/build/outputs/apk
destination: apks
- save_cache:
paths:
- ~/.android
- ~/.gradle
- ~/android
key: v1-android-{{ checksum "build.gradle" }}

View File

@ -83,7 +83,14 @@ android {
applicationIdSuffix ".debug"
resValue "string", "provider_authority", "de.danoeh.antennapod.debug.provider"
buildConfigField STRING, FLATTR_APP_KEY, mFlattrAppKey
buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
buildConfigField STRING, FLATTR_APP_SECRET, mFlattrAppSecret
dexcount {
if (project.hasProperty("enableDexcountInDebug")) {
runOnEachPackage enableDexcountInDebug.toBoolean()
} else { // default to not running dexcount
runOnEachPackage false
}
}
}
release {
resValue "string", "provider_authority", "de.danoeh.antennapod.provider"
@ -139,6 +146,7 @@ dependencies {
implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:gridlayout-v7:$supportVersion"
implementation "com.android.support:percent:$supportVersion"
implementation "com.android.support:recyclerview-v7:$supportVersion"
@ -172,6 +180,7 @@ dependencies {
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
implementation 'com.github.mfietz:fyydlin:v0.3'
implementation 'com.github.ByteHamster:SearchPreference:v1.0.8'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
}

View File

@ -118,12 +118,13 @@
</activity>
<service
android:name=".service.PlayerWidgetService"
android:name=".core.service.PlayerWidgetJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true"
android:exported="false">
</service>
<receiver android:name=".receiver.PlayerWidget">
<receiver android:name=".core.receiver.PlayerWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>

View File

@ -21,8 +21,7 @@ import java.nio.charset.Charset;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import rx.Observable;
import rx.Subscriber;
import rx.Single;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
@ -34,10 +33,8 @@ public class AboutActivity extends AppCompatActivity {
private static final String TAG = AboutActivity.class.getSimpleName();
private WebView webview;
private LinearLayout webviewContainer;
private int depth = 0;
private WebView webView;
private LinearLayout webViewContainer;
private Subscription subscription;
@Override
@ -46,28 +43,25 @@ public class AboutActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayShowHomeEnabled(true);
setContentView(R.layout.about);
webviewContainer = (LinearLayout) findViewById(R.id.webvContainer);
webview = (WebView) findViewById(R.id.webvAbout);
webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
webViewContainer = (LinearLayout) findViewById(R.id.webViewContainer);
webView = (WebView) findViewById(R.id.webViewAbout);
webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
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) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
webview.setBackgroundColor(Color.TRANSPARENT);
webView.setBackgroundColor(Color.TRANSPARENT);
}
webview.setWebViewClient(new WebViewClient() {
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(url.startsWith("http")) {
depth++;
return false;
} else {
if (!url.startsWith("http")) {
url = url.replace("file:///android_asset/", "");
loadAsset(url);
return true;
}
return false;
}
});
@ -75,7 +69,7 @@ public class AboutActivity extends AppCompatActivity {
}
private void loadAsset(String filename) {
subscription = Observable.create((Observable.OnSubscribe<String>) subscriber -> {
subscription = Single.create(subscriber -> {
InputStream input = null;
try {
TypedArray res = AboutActivity.this.getTheme().obtainStyledAttributes(
@ -85,8 +79,7 @@ public class AboutActivity extends AppCompatActivity {
res.recycle();
input = getAssets().open(filename);
String webViewData = IOUtils.toString(input, Charset.defaultCharset());
if(!webViewData.startsWith("<!DOCTYPE html>")) {
//webViewData = webViewData.replace("\n\n", "</p><p>");
if (!webViewData.startsWith("<!DOCTYPE html>")) {
webViewData = webViewData.replace("%", "&#37;");
webViewData =
"<!DOCTYPE html>" +
@ -106,35 +99,29 @@ public class AboutActivity extends AppCompatActivity {
" </style>" +
"</head><body><p>" + webViewData + "</p></body></html>";
webViewData = webViewData.replace("\n", "<br/>");
depth++;
} else {
depth = 0;
}
webViewData = String.format(webViewData, colorString);
subscriber.onNext(webViewData);
subscriber.onSuccess(webViewData);
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
subscriber.onError(e);
} finally {
IOUtils.closeQuietly(input);
}
subscriber.onCompleted();
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
webviewData ->
webview.loadDataWithBaseURL("file:///android_asset/", webviewData, "text/html", "utf-8", "about:blank"),
webViewData ->
webView.loadDataWithBaseURL("file:///android_asset/", webViewData.toString(), "text/html", "utf-8", "file:///android_asset/" + filename.toString()),
error -> Log.e(TAG, Log.getStackTraceString(error))
);
}
@Override
public void onBackPressed() {
Log.d(TAG, "depth: " + depth);
if(depth == 1) {
loadAsset("about.html");
} else if(depth > 1) {
webview.goBack();
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
@ -156,9 +143,9 @@ public class AboutActivity extends AppCompatActivity {
if(subscription != null) {
subscription.unsubscribe();
}
if (webviewContainer != null && webview != null) {
webviewContainer.removeAllViews();
webview.destroy();
if (webViewContainer != null && webView != null) {
webViewContainer.removeAllViews();
webView.destroy();
}
}
}

View File

@ -13,6 +13,7 @@ import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
/**
@ -34,14 +35,13 @@ public class AudioplayerActivity extends MediaplayerInfoActivity {
Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath());
ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
MediaType.AUDIO);
Intent launchIntent = new Intent(this, PlaybackService.class);
launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
true);
launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
true);
startService(launchIntent);
new PlaybackServiceStarter(this, media)
.startWhenPrepared(true)
.shouldStream(false)
.prepareImmediately(true)
.start();
} else if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (intent.getComponent() != null &&

View File

@ -8,7 +8,7 @@ import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.support.design.widget.Snackbar;
import android.support.v4.content.IntentCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
@ -39,7 +39,10 @@ public class ImportExportActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayShowHomeEnabled(true);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowHomeEnabled(true);
}
setContentView(R.layout.import_export_activity);
findViewById(R.id.button_export).setOnClickListener(view -> backup());
@ -125,7 +128,7 @@ public class ImportExportActivity extends AppCompatActivity {
d.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
Intent intent = new Intent(getApplicationContext(), SplashActivity.class);
ComponentName cn = intent.getComponent();
Intent mainIntent = IntentCompat.makeRestartActivityTask(cn);
Intent mainIntent = Intent.makeRestartActivityTask(cn);
startActivity(mainIntent);
});
d.show();

View File

@ -30,6 +30,8 @@ import android.widget.ListView;
import com.bumptech.glide.Glide;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
@ -200,6 +202,8 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
transaction.commit();
checkFirstLaunch();
NotificationUtils.createChannels(this);
UserPreferences.restartUpdateAlarm(false);
}
private void saveLastNavFragment(String tag) {
@ -739,6 +743,15 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
loadData();
}
public void onEventMainThread(ServiceEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
switch(event.action) {
case SERVICE_STARTED:
externalPlayerFragment.connectToPlaybackService();
break;
}
}
public void onEventMainThread(ProgressEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
switch(event.action) {

View File

@ -34,6 +34,7 @@ import com.joanzapata.iconify.fonts.FontAwesomeIcons;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -42,6 +43,7 @@ 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.FeedItemUtil;
import de.danoeh.antennapod.core.util.Flavors;
import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
@ -270,6 +272,9 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
controller.release();
}
controller = newPlaybackController();
setupGUI();
loadMediaInfo();
onPositionObserverUpdate();
}
@Override
@ -320,11 +325,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
((FeedMedia) media).getItem().getFlattrStatus().flattrable()
);
boolean hasWebsiteLink = media != null && media.getWebsiteLink() != null;
boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null );
menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink);
boolean isItemAndHasLink = isFeedMedia &&
((FeedMedia) media).getItem() != null && ((FeedMedia) media).getItem().getLink() != null;
ShareUtils.hasLinkToShare(((FeedMedia) media).getItem());
menu.findItem(R.id.share_link_item).setVisible(isItemAndHasLink);
menu.findItem(R.id.share_link_with_position_item).setVisible(isItemAndHasLink);
@ -560,7 +565,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
});
break;
case R.id.visit_website_item:
Uri uri = Uri.parse(media.getWebsiteLink());
Uri uri = Uri.parse(getWebsiteLinkWithFallback(media));
startActivity(new Intent(Intent.ACTION_VIEW, uri));
break;
case R.id.support_item:
@ -603,16 +608,37 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}
}
private static String getWebsiteLinkWithFallback(Playable media) {
if (media == null) {
return null;
} else if (media.getWebsiteLink() != null) {
return media.getWebsiteLink();
} else if (media instanceof FeedMedia) {
return FeedItemUtil.getLinkWithFallback(((FeedMedia)media).getItem());
}
return null;
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
StorageUtils.checkStorageAvailability(this);
if(controller != null) {
if (controller != null) {
controller.init();
}
}
public void onEventMainThread(ServiceEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
if (event.action == ServiceEvent.Action.SERVICE_STARTED) {
if (controller != null) {
controller.init();
}
}
}
/**
* Called by 'handleStatus()' when the PlaybackService is waiting for
* a video surface.
@ -853,6 +879,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
if(controller == null) {
return;
}
controller.init();
controller.playPause();
}

View File

@ -1,13 +1,12 @@
package de.danoeh.antennapod.activity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
@ -15,6 +14,9 @@ import android.widget.FrameLayout;
import java.lang.ref.WeakReference;
import com.bytehamster.lib.preferencesearch.SearchPreference;
import com.bytehamster.lib.preferencesearch.SearchPreferenceResult;
import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.preferences.PreferenceController;
@ -23,19 +25,24 @@ import de.danoeh.antennapod.preferences.PreferenceController;
* PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see
* PreferenceController.
*/
public class PreferenceActivity extends AppCompatActivity {
public class PreferenceActivity extends AppCompatActivity implements SearchPreferenceResultListener {
public static final String PARAM_RESOURCE = "resource";
private static WeakReference<PreferenceActivity> instance;
private PreferenceController preferenceController;
private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() {
private PreferenceFragment fragment;
private PreferenceFragmentCompat fragment;
@Override
public void setFragment(PreferenceFragment fragment) {
public void setFragment(PreferenceFragmentCompat fragment) {
this.fragment = fragment;
}
@Override
public PreferenceFragmentCompat getFragment() {
return fragment;
}
@Override
public Preference findPreference(CharSequence key) {
return fragment.findPreference(key);
@ -47,7 +54,7 @@ public class PreferenceActivity extends AppCompatActivity {
}
@Override
public Activity getActivity() {
public AppCompatActivity getActivity() {
return PreferenceActivity.this;
}
};
@ -77,12 +84,21 @@ public class PreferenceActivity extends AppCompatActivity {
// since the MainFragment depends on the preferenceController already being created
preferenceController = new PreferenceController(preferenceUI);
PreferenceFragment prefFragment = new MainFragment();
showPreferenceScreen(R.xml.preferences, false);
}
private void showPreferenceScreen(int screen, boolean addHistory) {
PreferenceFragmentCompat prefFragment = new MainFragment();
preferenceUI.setFragment(prefFragment);
Bundle args = new Bundle();
args.putInt(PARAM_RESOURCE, R.xml.preferences);
args.putInt(PARAM_RESOURCE, screen);
prefFragment.setArguments(args);
getFragmentManager().beginTransaction().replace(R.id.content, prefFragment).commit();
if (addHistory) {
getSupportFragmentManager().beginTransaction().replace(R.id.content, prefFragment)
.addToBackStack(getString(PreferenceController.getTitleOfPage(screen))).commit();
} else {
getSupportFragmentManager().beginTransaction().replace(R.id.content, prefFragment).commit();
}
}
@Override
@ -101,10 +117,10 @@ public class PreferenceActivity extends AppCompatActivity {
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if (getFragmentManager().getBackStackEntryCount() == 0) {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
finish();
} else {
getFragmentManager().popBackStack();
getSupportFragmentManager().popBackStack();
}
return true;
default:
@ -112,13 +128,23 @@ public class PreferenceActivity extends AppCompatActivity {
}
}
public static class MainFragment extends PreferenceFragment {
@Override
public void onSearchResultClicked(SearchPreferenceResult result) {
showPreferenceScreen(result.getResourceFile(), true);
result.highlight(preferenceUI.getFragment());
}
public static class MainFragment extends PreferenceFragmentCompat {
private int screen;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
screen = getArguments().getInt(PARAM_RESOURCE);
addPreferencesFromResource(screen);
PreferenceActivity activity = instance.get();
@ -133,35 +159,16 @@ public class PreferenceActivity extends AppCompatActivity {
super.onResume();
PreferenceActivity activity = instance.get();
if(activity != null && activity.preferenceController != null) {
activity.setTitle(getTitle(screen));
activity.setTitle(PreferenceController.getTitleOfPage(screen));
activity.preferenceUI.setFragment(this);
activity.preferenceController.onResume(screen);
}
}
private int getTitle(int preferences) {
switch (preferences) {
case R.xml.preferences_network:
return R.string.network_pref;
case R.xml.preferences_autodownload:
return R.string.pref_automatic_download_title;
case R.xml.preferences_playback:
return R.string.playback_pref;
case R.xml.preferences_storage:
return R.string.storage_pref;
case R.xml.preferences_user_interface:
return R.string.user_interface_label;
case R.xml.preferences_integrations:
return R.string.integrations_label;
default:
return R.string.settings_label;
}
}
@Override
public void onPause() {
PreferenceActivity activity = instance.get();
if (screen == R.xml.preferences_integrations) {
if (screen == R.xml.preferences_gpodder) {
activity.preferenceController.unregisterGpodnet();
}
super.onPause();

View File

@ -30,6 +30,7 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.view.AspectRatioVideoView;
import java.lang.ref.WeakReference;
@ -82,14 +83,12 @@ public class VideoplayerActivity extends MediaplayerActivity {
Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath());
ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
MediaType.VIDEO);
Intent launchIntent = new Intent(this, PlaybackService.class);
launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
true);
launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, false);
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
true);
startService(launchIntent);
new PlaybackServiceStarter(this, media)
.startWhenPrepared(true)
.shouldStream(false)
.prepareImmediately(true)
.start();
} else if (PlaybackService.isCasting()) {
Intent intent = PlaybackService.getPlayerActivityIntent(this);
if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {

View File

@ -6,6 +6,7 @@ import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import org.apache.commons.lang3.Validate;
import de.danoeh.antennapod.R;
@ -80,13 +81,19 @@ public class DefaultActionButtonCallback implements ActionButtonCallback {
Toast.makeText(context, R.string.download_canceled_msg, Toast.LENGTH_LONG).show();
}
} else { // media is downloaded
if (item.hasMedia() && item.getMedia().isCurrentlyPlaying()) {
if (media.isCurrentlyPlaying()) {
new PlaybackServiceStarter(context, media)
.startWhenPrepared(true)
.shouldStream(false)
.start();
context.sendBroadcast(new Intent(PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE));
}
else if (item.hasMedia() && item.getMedia().isCurrentlyPaused()) {
} else if (media.isCurrentlyPaused()) {
new PlaybackServiceStarter(context, media)
.startWhenPrepared(true)
.shouldStream(false)
.start();
context.sendBroadcast(new Intent(PlaybackService.ACTION_RESUME_PLAY_CURRENT_EPISODE));
}
else {
} else {
DBTasks.playMedia(context, media, false, true, false);
}
}

View File

@ -100,8 +100,10 @@ public class DownloadedEpisodesListAdapter extends BaseAdapter {
FeedItem.State state = item.getState();
if (state == FeedItem.State.PLAYING) {
holder.butSecondary.setEnabled(false);
holder.butSecondary.setAlpha(0.5f);
} else {
holder.butSecondary.setEnabled(true);
holder.butSecondary.setAlpha(1.0f);
}
holder.butSecondary.setFocusable(false);
holder.butSecondary.setTag(item);

View File

@ -37,7 +37,6 @@ public class ExternalPlayerFragment extends Fragment {
private ImageButton butPlay;
private TextView mFeedName;
private ProgressBar mProgressBar;
private PlaybackController controller;
public ExternalPlayerFragment() {
@ -83,6 +82,11 @@ public class ExternalPlayerFragment extends Fragment {
controller.playPause();
}
});
loadMediaInfo();
}
public void connectToPlaybackService() {
controller.init();
}
private PlaybackController setupPlaybackController() {
@ -164,36 +168,35 @@ public class ExternalPlayerFragment extends Fragment {
private boolean loadMediaInfo() {
Log.d(TAG, "Loading media info");
if (controller != null && controller.serviceAvailable()) {
Playable media = controller.getMedia();
if (media != null) {
txtvTitle.setText(media.getEpisodeTitle());
mFeedName.setText(media.getFeedTitle());
mProgressBar.setProgress((int)
((double) controller.getPosition() / controller.getDuration() * 100));
if (controller == null) {
Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!");
return false;
}
Glide.with(getActivity())
.load(media.getImageLocation())
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate()
.into(imgvCover);
Playable media = controller.getMedia();
if (media != null) {
txtvTitle.setText(media.getEpisodeTitle());
mFeedName.setText(media.getFeedTitle());
onPositionObserverUpdate();
fragmentLayout.setVisibility(View.VISIBLE);
if (controller.isPlayingVideoLocally()) {
butPlay.setVisibility(View.GONE);
} else {
butPlay.setVisibility(View.VISIBLE);
}
return true;
Glide.with(getActivity())
.load(media.getImageLocation())
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate()
.into(imgvCover);
fragmentLayout.setVisibility(View.VISIBLE);
if (controller.isPlayingVideoLocally()) {
butPlay.setVisibility(View.GONE);
} else {
Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!");
return false;
butPlay.setVisibility(View.VISIBLE);
}
return true;
} else {
Log.w(TAG, "loadMediaInfo was called while playbackService was null!");
Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!");
return false;
}
}

View File

@ -34,6 +34,7 @@ import com.bumptech.glide.Glide;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.widget.IconButton;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.NetworkUtils;
import org.apache.commons.lang3.ArrayUtils;
@ -432,6 +433,16 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
butAction1Text = R.string.download_label;
}
}
FeedItem.State state = item.getState();
if (butAction2Text == R.string.delete_label && state == FeedItem.State.PLAYING) {
butAction2.setEnabled(false);
butAction2.setAlpha(0.5f);
} else {
butAction2.setEnabled(true);
butAction2.setAlpha(1.0f);
}
if(butAction1Icon != null && butAction1Text != 0) {
butAction1.setText(butAction1Icon +"\u0020\u0020" + getActivity().getString(butAction1Text));
Iconify.addIcons(butAction1);

View File

@ -327,6 +327,15 @@ public class QueueFragment extends Fragment {
case R.id.queue_sort_feed_title_desc:
QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_DESC, true);
return true;
case R.id.queue_sort_random:
QueueSorter.sort(getActivity(), QueueSorter.Rule.RANDOM, true);
return true;
case R.id.queue_sort_smart_shuffle_asc:
QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_ASC, true);
return true;
case R.id.queue_sort_smart_shuffle_desc:
QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_DESC, true);
return true;
default:
return false;
}
@ -525,14 +534,15 @@ public class QueueFragment extends Fragment {
private void refreshInfoBar() {
String info = queue.size() + getString(R.string.episodes_suffix);
if(queue.size() > 0) {
long duration = 0;
long timeLeft = 0;
for(FeedItem item : queue) {
if(item.getMedia() != null) {
duration += item.getMedia().getDuration();
timeLeft += item.getMedia().getDuration() - item.getMedia().getPosition();
}
}
info += " \u2022 ";
info += Converter.getDurationStringLocalized(getActivity(), duration);
info += getString(R.string.time_left_label);
info += Converter.getDurationStringLocalized(getActivity(), timeLeft);
}
infoBar.setText(info);
}

View File

@ -17,6 +17,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.ShareUtils;
@ -86,7 +87,7 @@ public class FeedItemMenuHandler {
mi.setItemVisibility(R.id.add_to_queue_item, false);
}
if (!showExtendedMenu || selectedItem.getLink() == null) {
if (!showExtendedMenu || !ShareUtils.hasLinkToShare(selectedItem)) {
mi.setItemVisibility(R.id.visit_website_item, false);
mi.setItemVisibility(R.id.share_link_item, false);
mi.setItemVisibility(R.id.share_link_with_position_item, false);
@ -216,7 +217,7 @@ public class FeedItemMenuHandler {
DBWriter.setFeedItemAutoDownload(selectedItem, false);
break;
case R.id.visit_website_item:
Uri uri = Uri.parse(selectedItem.getLink());
Uri uri = Uri.parse(FeedItemUtil.getLinkWithFallback(selectedItem));
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if(IntentUtils.isCallable(context, intent)) {
context.startActivity(intent);

View File

@ -4,13 +4,14 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.TextView;
import de.danoeh.antennapod.R;
public class MasterSwitchPreference extends SwitchCompatPreference {
public class MasterSwitchPreference extends SwitchPreference {
public MasterSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@ -29,15 +30,16 @@ public class MasterSwitchPreference extends SwitchCompatPreference {
super(context);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
TypedValue typedValue = new TypedValue();
getContext().getTheme().resolveAttribute(R.attr.master_switch_background, typedValue, true);
view.setBackgroundColor(typedValue.data);
holder.itemView.setBackgroundColor(typedValue.data);
TextView title = (TextView) view.findViewById(android.R.id.title);
TextView title = (TextView) holder.findViewById(android.R.id.title);
if (title != null) {
title.setTypeface(title.getTypeface(), Typeface.BOLD);
}

View File

@ -3,7 +3,6 @@ package de.danoeh.antennapod.preferences;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.app.TimePickerDialog;
import android.content.ActivityNotFoundException;
@ -18,31 +17,29 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
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.FileProvider;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceManager;
import android.support.v7.preference.PreferenceScreen;
import android.text.Html;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.bytehamster.lib.preferencesearch.SearchConfiguration;
import com.bytehamster.lib.preferencesearch.SearchPreference;
import de.danoeh.antennapod.activity.AboutActivity;
import com.afollestad.materialdialogs.prefs.MaterialListPreference;
import de.danoeh.antennapod.activity.ImportExportActivity;
import de.danoeh.antennapod.activity.MediaplayerActivity;
import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
@ -99,8 +96,9 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
private static final String PREF_SCREEN_INTEGRATIONS = "prefScreenIntegrations";
private static final String PREF_SCREEN_STORAGE = "prefScreenStorage";
private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings";
private static final String PREF_SCREEN_FLATTR = "prefFlattrSettings";
private static final String PREF_SCREEN_GPODDER = "prefGpodderSettings";
private static final String PREF_FLATTR_SETTINGS = "prefFlattrSettings";
private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate";
private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess";
private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs";
@ -176,6 +174,12 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
case R.xml.preferences_integrations:
setupIntegrationsScreen();
break;
case R.xml.preferences_flattr:
setupFlattrScreen();
break;
case R.xml.preferences_gpodder:
setupGpodderScreen();
break;
case R.xml.preferences_storage:
setupStorageScreen();
break;
@ -295,7 +299,20 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
private void setupIntegrationsScreen() {
final Activity activity = ui.getActivity();
final AppCompatActivity activity = ui.getActivity();
ui.findPreference(PREF_SCREEN_FLATTR).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_flattr, activity);
return true;
});
ui.findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_gpodder, activity);
return true;
});
}
private void setupFlattrScreen() {
final AppCompatActivity activity = ui.getActivity();
ui.findPreference(PreferenceController.PREF_FLATTR_REVOKE).setOnPreferenceClickListener(
preference -> {
@ -304,6 +321,29 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
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);
checkFlattrItemVisibility();
}
});
return true;
});
}
private void setupGpodderScreen() {
final AppCompatActivity activity = ui.getActivity();
ui.findPreference(PreferenceController.PREF_GPODNET_SETLOGIN_INFORMATION)
.setOnPreferenceClickListener(preference -> {
AuthenticationDialog dialog = new AuthenticationDialog(activity,
@ -351,24 +391,6 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
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);
checkFlattrItemVisibility();
}
});
return true;
});
}
private void setupPlaybackScreen() {
@ -390,7 +412,7 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
return true;
});
if (!PictureInPictureUtil.supportsPictureInPicture(activity)) {
MaterialListPreference behaviour = (MaterialListPreference) ui.findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR);
ListPreference behaviour = (ListPreference) ui.findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR);
behaviour.setEntries(R.array.video_background_behavior_options_without_pip);
behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip);
}
@ -427,9 +449,11 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
private void setupNetworkScreen() {
final Activity activity = ui.getActivity();
ui.findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_autodownload, activity));
final AppCompatActivity activity = ui.getActivity();
ui.findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_autodownload, activity);
return true;
});
ui.findPreference(UserPreferences.PREF_UPDATE_INTERVAL)
.setOnPreferenceClickListener(preference -> {
showUpdateIntervalTimePreferencesDialog();
@ -453,33 +477,6 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
);
// 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(PREF_PROXY).setOnPreferenceClickListener(preference -> {
ProxyDialog dialog = new ProxyDialog(ui.getActivity());
dialog.createDialog().show();
@ -488,17 +485,28 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
private void setupMainScreen() {
final Activity activity = ui.getActivity();
ui.findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_user_interface, activity));
ui.findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_playback, activity));
ui.findPreference(PREF_SCREEN_NETWORK).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_network, activity));
ui.findPreference(PREF_SCREEN_INTEGRATIONS).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_integrations, activity));
ui.findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference ->
openScreen(R.xml.preferences_storage, activity));
final AppCompatActivity activity = ui.getActivity();
setupSearch();
ui.findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_user_interface, activity);
return true;
});
ui.findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_playback, activity);
return true;
});
ui.findPreference(PREF_SCREEN_NETWORK).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_network, activity);
return true;
});
ui.findPreference(PREF_SCREEN_INTEGRATIONS).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_integrations, activity);
return true;
});
ui.findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> {
openScreen(R.xml.preferences_storage, activity);
return true;
});
ui.findPreference(PreferenceController.PREF_ABOUT).setOnPreferenceClickListener(
preference -> {
@ -545,15 +553,74 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
});
}
private boolean openScreen(int preferences, Activity activity) {
Fragment prefFragment = new PreferenceActivity.MainFragment();
private void setupSearch() {
final AppCompatActivity activity = ui.getActivity();
SearchPreference searchPreference = (SearchPreference) ui.findPreference("searchPreference");
SearchConfiguration config = searchPreference.getSearchConfiguration();
config.setActivity(activity);
config.setFragmentContainerViewId(R.id.content);
config.setBreadcrumbsEnabled(true);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
.addFile(R.xml.preferences_user_interface);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_playback))
.addFile(R.xml.preferences_playback);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_network))
.addFile(R.xml.preferences_network);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_storage))
.addFile(R.xml.preferences_storage);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_network))
.addBreadcrumb(R.string.automation)
.addBreadcrumb(getTitleOfPage(R.xml.preferences_autodownload))
.addFile(R.xml.preferences_autodownload);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_integrations))
.addBreadcrumb(getTitleOfPage(R.xml.preferences_gpodder))
.addFile(R.xml.preferences_gpodder);
config.index()
.addBreadcrumb(getTitleOfPage(R.xml.preferences_integrations))
.addBreadcrumb(getTitleOfPage(R.xml.preferences_flattr))
.addFile(R.xml.preferences_flattr);
}
public PreferenceFragmentCompat openScreen(int preferences, AppCompatActivity activity) {
PreferenceFragmentCompat prefFragment = new PreferenceActivity.MainFragment();
Bundle args = new Bundle();
args.putInt(PARAM_RESOURCE, preferences);
prefFragment.setArguments(args);
activity.getFragmentManager().beginTransaction()
activity.getSupportFragmentManager().beginTransaction()
.replace(R.id.content, prefFragment)
.addToBackStack(TAG).commit();
return true;
return prefFragment;
}
public static int getTitleOfPage(int preferences) {
switch (preferences) {
case R.xml.preferences_network:
return R.string.network_pref;
case R.xml.preferences_autodownload:
return R.string.pref_automatic_download_title;
case R.xml.preferences_playback:
return R.string.playback_pref;
case R.xml.preferences_storage:
return R.string.storage_pref;
case R.xml.preferences_user_interface:
return R.string.user_interface_label;
case R.xml.preferences_integrations:
return R.string.integrations_label;
case R.xml.preferences_flattr:
return R.string.flattr_label;
case R.xml.preferences_gpodder:
return R.string.gpodnet_main_label;
default:
return R.string.settings_label;
}
}
private boolean export(ExportWriter exportWriter) {
@ -623,9 +690,14 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
setDataFolderText();
break;
case R.xml.preferences_integrations:
setIntegrationsItemVisibility();
return;
case R.xml.preferences_flattr:
checkFlattrItemVisibility();
break;
case R.xml.preferences_gpodder:
GpodnetPreferences.registerOnSharedPreferenceChangeListener(gpoddernetListener);
updateGpodnetPreferenceScreen();
checkFlattrItemVisibility();
break;
case R.xml.preferences_playback:
checkSonicItemVisibility();
@ -792,10 +864,13 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
}
}
private void setIntegrationsItemVisibility() {
ui.findPreference(PreferenceController.PREF_SCREEN_FLATTR).setEnabled(FlattrUtils.hasAPICredentials());
}
@SuppressWarnings("deprecation")
private void checkFlattrItemVisibility() {
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);
@ -1109,7 +1184,8 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
public interface PreferenceUI {
void setFragment(PreferenceFragment fragment);
void setFragment(PreferenceFragmentCompat fragment);
PreferenceFragmentCompat getFragment();
/**
* Finds a preference based on its key.
@ -1118,6 +1194,6 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
PreferenceScreen getPreferenceScreen();
Activity getActivity();
AppCompatActivity getActivity();
}
}

View File

@ -1,37 +0,0 @@
package de.danoeh.antennapod.preferences;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
import de.danoeh.antennapod.R;
public class SwitchCompatPreference extends CheckBoxPreference {
public SwitchCompatPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SwitchCompatPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public SwitchCompatPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SwitchCompatPreference(Context context) {
super(context);
init();
}
private void init() {
setWidgetLayoutResource(R.layout.preference_switch_layout);
}
}

View File

@ -1,244 +0,0 @@
package de.danoeh.antennapod.service;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.RemoteViews;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
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.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.receiver.PlayerWidget;
/**
* Updates the state of the player widget
*/
public class PlayerWidgetService extends Service {
private static final String TAG = "PlayerWidgetService";
private PlaybackService playbackService;
/**
* Controls write access to playbackservice reference
*/
private final Object psLock = new Object();
/**
* True while service is updating the widget
*/
private volatile boolean isUpdating;
public PlayerWidgetService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Service created");
isUpdating = false;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Service is about to be destroyed");
if (playbackService != null) {
Playable playable = playbackService.getPlayable();
if (playable != null && playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
if (media.hasAlmostEnded()) {
Log.d(TAG, "smart mark as read");
FeedItem item = media.getItem();
DBWriter.markItemPlayed(item, FeedItem.PLAYED, false);
DBWriter.removeQueueItem(this, item, false);
DBWriter.addItemToPlaybackHistory(media);
if (item.getFeed().getPreferences().getCurrentAutoDelete() &&
(!item.isTagged(FeedItem.TAG_FAVORITE) || !UserPreferences.shouldFavoriteKeepEpisode())) {
Log.d(TAG, "Delete " + media.toString());
DBWriter.deleteFeedMediaOfItem(this, media.getId());
}
}
}
}
try {
unbindService(mConnection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "IllegalArgumentException when trying to unbind service");
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!isUpdating) {
if (playbackService == null && PlaybackService.isRunning) {
bindService(new Intent(this, PlaybackService.class),
mConnection, 0);
} else {
startViewUpdaterIfNotRunning();
}
} else {
Log.d(TAG, "Service was called while updating. Ignoring update request");
}
return Service.START_NOT_STICKY;
}
private void updateViews() {
isUpdating = true;
ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
RemoteViews views = new RemoteViews(getPackageName(),
R.layout.player_widget);
PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this), 0);
Intent startApp = new Intent(getBaseContext(), MainActivity.class);
startApp.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startApp.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, QueueFragment.TAG);
PendingIntent startAppPending = PendingIntent.getActivity(getBaseContext(), 0, startApp, PendingIntent.FLAG_UPDATE_CURRENT);
boolean nothingPlaying = false;
if (playbackService != null) {
final Playable media = playbackService.getPlayable();
if (media != null) {
PlayerStatus status = playbackService.getStatus();
views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
String progressString = getProgressString();
if (progressString != null) {
views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
views.setTextViewText(R.id.txtvProgress, progressString);
}
if (status == PlayerStatus.PLAYING) {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp);
if (Build.VERSION.SDK_INT >= 15) {
views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
}
} else {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
if (Build.VERSION.SDK_INT >= 15) {
views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
}
}
views.setOnClickPendingIntent(R.id.butPlay,
createMediaButtonIntent());
} else {
nothingPlaying = true;
}
} else {
nothingPlaying = true;
}
if (nothingPlaying) {
// start the app if they click anything
views.setOnClickPendingIntent(R.id.layout_left, startAppPending);
views.setOnClickPendingIntent(R.id.butPlay, startAppPending);
views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
views.setTextViewText(R.id.txtvTitle,
this.getString(R.string.no_media_playing_label));
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
}
manager.updateAppWidget(playerWidget, views);
isUpdating = false;
}
/**
* Creates an intent which fakes a mediabutton press
*/
private PendingIntent createMediaButtonIntent() {
KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
Intent startingIntent = new Intent(
MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
}
private String getProgressString() {
int position = playbackService.getCurrentPosition();
int duration = playbackService.getDuration();
if (position > 0 && duration > 0) {
return Converter.getDurationStringLong(position) + " / "
+ Converter.getDurationStringLong(duration);
} else {
return null;
}
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Connection to service established");
synchronized (psLock) {
if(service instanceof PlaybackService.LocalBinder) {
playbackService = ((PlaybackService.LocalBinder) service).getService();
startViewUpdaterIfNotRunning();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (psLock) {
playbackService = null;
Log.d(TAG, "Disconnected from service");
}
}
};
private void startViewUpdaterIfNotRunning() {
if (!isUpdating) {
ViewUpdater updateThread = new ViewUpdater(this);
updateThread.start();
}
}
class ViewUpdater extends Thread {
private static final String THREAD_NAME = "ViewUpdater";
private final PlayerWidgetService service;
public ViewUpdater(PlayerWidgetService service) {
super();
setName(THREAD_NAME);
this.service = service;
}
@Override
public void run() {
synchronized (psLock) {
service.updateViews();
}
}
}
}

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webvContainer"
android:id="@+id/webViewContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<WebView
android:id="@+id/webvAbout"
android:id="@+id/webViewAbout"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -44,6 +44,7 @@
android:layout_centerVertical="true"
android:contentDescription="@string/pause_label"
android:background="?attr/selectableItemBackground"
android:src="?attr/av_play_big"
tools:src="@drawable/ic_play_arrow_white_36dp"/>
<TextView

View File

@ -110,9 +110,9 @@
android:layout_centerHorizontal="true"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/pause_label"
android:src="?attr/av_pause"
android:src="?attr/av_play"
android:scaleType="fitCenter"
tools:src="@drawable/ic_pause_white_36dp"
tools:src="@drawable/ic_play_arrow_white_24dp"
tools:background="@android:color/holo_green_dark" />
<ImageButton

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.SwitchCompat
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false" />

View File

@ -90,6 +90,25 @@
android:title="@string/descending"/>
</menu>
</item>
<item
android:id="@+id/queue_sort_random"
android:title="@string/random">
</item>
<item
android:id="@+id/queue_sort_smart_shuffle"
android:title="@string/smart_shuffle">
<menu>
<item
android:id="@+id/queue_sort_smart_shuffle_asc"
android:title="@string/ascending"/>
<item
android:id="@+id/queue_sort_smart_shuffle_desc"
android:title="@string/descending"/>
</menu>
</item>
</menu>
</item>

View File

@ -1,10 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<com.bytehamster.lib.preferencesearch.SearchPreference
android:key="searchPreference" />
<Preference
android:key="prefScreenInterface"
android:title="@string/user_interface_label"
android:icon="?attr/type_video" />
android:key="prefScreenInterface"
android:title="@string/user_interface_label"
android:icon="?attr/type_video" />
<Preference
android:key="prefScreenPlayback"

View File

@ -1,20 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
<de.danoeh.antennapod.preferences.MasterSwitchPreference
android:key="prefEnableAutoDl"
android:title="@string/pref_automatic_download_title"
search:summary="@string/pref_automatic_download_sum"
android:defaultValue="false"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:defaultValue="25"
android:entries="@array/episode_cache_size_entries"
android:key="prefEpisodeCacheSize"
android:title="@string/pref_episode_cache_title"
android:entryValues="@array/episode_cache_size_values"
app:useStockLayout="true"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:defaultValue="-1"
android:entries="@array/episode_cleanup_entries"
android:key="prefEpisodeCleanup"
@ -22,17 +24,17 @@
android:summary="@string/pref_episode_cleanup_summary"
android:entryValues="@array/episode_cleanup_values"
app:useStockLayout="true"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:key="prefEnableAutoDownloadOnBattery"
android:title="@string/pref_automatic_download_on_battery_title"
android:summary="@string/pref_automatic_download_on_battery_sum"
android:defaultValue="true"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:key="prefEnableAutoDownloadOnMobile"
android:title="@string/pref_autodl_allow_on_mobile_title"
android:summary="@string/pref_autodl_allow_on_mobile_sum"
android:defaultValue="false"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:key="prefEnableAutoDownloadWifiFilter"
android:title="@string/pref_autodl_wifi_filter_title"
android:summary="@string/pref_autodl_wifi_filter_sum"/>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:key="pref_flattr_authenticate"
android:summary="@string/pref_flattr_auth_sum"
android:title="@string/pref_flattr_auth_title">
<intent android:action=".activities.FlattrAuthActivity"/>
</PreferenceScreen>
<Preference
android:key="prefAutoFlattrPrefs"
android:summary="@string/pref_auto_flattr_sum"
android:title="@string/pref_auto_flattr_title"/>
<Preference
android:key="prefRevokeAccess"
android:summary="@string/pref_revokeAccess_sum"
android:title="@string/pref_revokeAccess_title"/>
</PreferenceScreen>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:key="pref_gpodnet_authenticate"
android:title="@string/pref_gpodnet_authenticate_title"
android:summary="@string/pref_gpodnet_authenticate_sum">
<intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
</PreferenceScreen>
<Preference
android:key="pref_gpodnet_setlogin_information"
android:title="@string/pref_gpodnet_setlogin_information_title"
android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
<Preference
android:key="pref_gpodnet_sync"
android:title="@string/pref_gpodnet_sync_changes_title"
android:summary="@string/pref_gpodnet_sync_changes_sum"/>
<Preference
android:key="pref_gpodnet_force_full_sync"
android:title="@string/pref_gpodnet_full_sync_title"
android:summary="@string/pref_gpodnet_full_sync_sum"/>
<Preference
android:key="pref_gpodnet_logout"
android:title="@string/pref_gpodnet_logout_title"/>
<Preference
android:key="pref_gpodnet_hostname"
android:title="@string/pref_gpodnet_sethostname_title"/>
<SwitchPreference
android:key="pref_gpodnet_notifications"
android:title="@string/pref_gpodnet_notifications_title"
android:summary="@string/pref_gpodnet_notifications_sum"
android:defaultValue="true"/>
</PreferenceScreen>

View File

@ -3,61 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen
<Preference
android:key="prefFlattrSettings"
android:title="@string/flattr_label"
android:summary="@string/flattr_summary">
<PreferenceScreen
android:key="pref_flattr_authenticate"
android:summary="@string/pref_flattr_auth_sum"
android:title="@string/pref_flattr_auth_title">
<intent android:action=".activities.FlattrAuthActivity"/>
</PreferenceScreen>
android:summary="@string/flattr_summary" />
<Preference
android:key="prefAutoFlattrPrefs"
android:summary="@string/pref_auto_flattr_sum"
android:title="@string/pref_auto_flattr_title"/>
<Preference
android:key="prefRevokeAccess"
android:summary="@string/pref_revokeAccess_sum"
android:title="@string/pref_revokeAccess_title"/>
</PreferenceScreen>
<PreferenceScreen
<Preference
android:key="prefGpodderSettings"
android:title="@string/gpodnet_main_label"
android:summary="@string/gpodnet_summary">
<PreferenceScreen
android:key="pref_gpodnet_authenticate"
android:title="@string/pref_gpodnet_authenticate_title"
android:summary="@string/pref_gpodnet_authenticate_sum">
<intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
</PreferenceScreen>
<Preference
android:key="pref_gpodnet_setlogin_information"
android:title="@string/pref_gpodnet_setlogin_information_title"
android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
<Preference
android:key="pref_gpodnet_sync"
android:title="@string/pref_gpodnet_sync_changes_title"
android:summary="@string/pref_gpodnet_sync_changes_sum"/>
<Preference
android:key="pref_gpodnet_force_full_sync"
android:title="@string/pref_gpodnet_full_sync_title"
android:summary="@string/pref_gpodnet_full_sync_sum"/>
<Preference
android:key="pref_gpodnet_logout"
android:title="@string/pref_gpodnet_logout_title"/>
<Preference
android:key="pref_gpodnet_hostname"
android:title="@string/pref_gpodnet_sethostname_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
android:key="pref_gpodnet_notifications"
android:title="@string/pref_gpodnet_notifications_title"
android:summary="@string/pref_gpodnet_notifications_sum"
android:defaultValue="true"/>
</PreferenceScreen>
android:summary="@string/gpodnet_summary" />
</PreferenceScreen>

View File

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
<PreferenceCategory android:title="@string/automation">
<Preference
android:key="prefAutoUpdateIntervall"
android:summary="@string/pref_autoUpdateIntervallOrTime_sum"
android:title="@string/pref_autoUpdateIntervallOrTime_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefMobileUpdate"
@ -16,17 +17,18 @@
<Preference
android:summary="@string/pref_automatic_download_sum"
android:key="prefAutoDownloadSettings"
android:title="@string/pref_automatic_download_title" />
android:title="@string/pref_automatic_download_title"
search:ignore="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/download_pref_details">
<com.afollestad.materialdialogs.prefs.MaterialEditTextPreference
<EditTextPreference
android:defaultValue="4"
android:inputType="number"
android:key="prefParallelDownloads"
android:title="@string/pref_parallel_downloads_title"
app:useStockLayout="true"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefShowDownloadReport"

View File

@ -4,39 +4,39 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/interruptions">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefPauseOnHeadsetDisconnect"
android:summary="@string/pref_pauseOnDisconnect_sum"
android:title="@string/pref_pauseOnHeadsetDisconnect_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:dependency="prefPauseOnHeadsetDisconnect"
android:key="prefUnpauseOnHeadsetReconnect"
android:summary="@string/pref_unpauseOnHeadsetReconnect_sum"
android:title="@string/pref_unpauseOnHeadsetReconnect_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:dependency="prefPauseOnHeadsetDisconnect"
android:key="prefUnpauseOnBluetoothReconnect"
android:summary="@string/pref_unpauseOnBluetoothReconnect_sum"
android:title="@string/pref_unpauseOnBluetoothReconnect_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefPauseForFocusLoss"
android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
android:title="@string/pref_pausePlaybackForFocusLoss_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefResumeAfterCall"
android:summary="@string/pref_resumeAfterCall_sum"
android:title="@string/pref_resumeAfterCall_title"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:defaultValue="stop"
android:entries="@array/video_background_behavior_options"
android:entryValues="@array/video_background_behavior_values"
@ -47,13 +47,13 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/buttons">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefHardwareForwardButtonSkips"
android:summary="@string/pref_hardwareForwardButtonSkips_sum"
android:title="@string/pref_hardwareForwardButtonSkips_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefHardwarePreviousButtonRestarts"
@ -74,25 +74,25 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/queue_label">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefEnqueueDownloaded"
android:summary="@string/pref_enqueue_downloaded_summary"
android:title="@string/pref_enqueue_downloaded_title" />
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefQueueAddToFront"
android:summary="@string/pref_queueAddToFront_sum"
android:title="@string/pref_queueAddToFront_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefFollowQueue"
android:summary="@string/pref_followQueue_sum"
android:title="@string/pref_followQueue_title"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:defaultValue="30"
android:entries="@array/smart_mark_as_played_values"
android:entryValues="@array/smart_mark_as_played_values"
@ -100,7 +100,7 @@
android:summary="@string/pref_smart_mark_as_played_sum"
android:title="@string/pref_smart_mark_as_played_title"
app:useStockLayout="true"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefSkipKeepsEpisode"
@ -109,7 +109,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/media_player">
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:defaultValue="sonic"
android:entries="@array/media_player_options"
android:key="prefMediaPlayer"
@ -120,7 +120,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/experimental_pref">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefCast"

View File

@ -13,13 +13,13 @@
android:key="prefImageCacheSize"
android:summary="@string/pref_image_cache_size_sum"
android:defaultValue="100"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefAutoDelete"
android:summary="@string/pref_auto_delete_sum"
android:title="@string/pref_auto_delete_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefFavoriteKeepsEpisode"

View File

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/appearance">
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:entryValues="@array/theme_values"
android:entries="@array/theme_options"
android:title="@string/pref_set_theme_title"
@ -16,7 +16,7 @@
android:key="prefHiddenDrawerItems"
android:summary="@string/pref_nav_drawer_items_sum"
android:title="@string/pref_nav_drawer_items_title"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:entryValues="@array/nav_drawer_feed_order_values"
android:entries="@array/nav_drawer_feed_order_options"
android:title="@string/pref_nav_drawer_feed_order_title"
@ -24,7 +24,7 @@
android:summary="@string/pref_nav_drawer_feed_order_sum"
android:defaultValue="0"
app:useStockLayout="true"/>
<com.afollestad.materialdialogs.prefs.MaterialListPreference
<ListPreference
android:entryValues="@array/nav_drawer_feed_counter_values"
android:entries="@array/nav_drawer_feed_counter_options"
android:title="@string/pref_nav_drawer_feed_counter_title"
@ -34,13 +34,13 @@
app:useStockLayout="true"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/external_elements">
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="false"
android:enabled="true"
android:key="prefExpandNotify"
android:summary="@string/pref_expandNotify_sum"
android:title="@string/pref_expandNotify_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefPersistNotify"
@ -50,7 +50,7 @@
android:key="prefCompactNotificationButtons"
android:summary="@string/pref_compact_notification_buttons_sum"
android:title="@string/pref_compact_notification_buttons_title"/>
<de.danoeh.antennapod.preferences.SwitchCompatPreference
<SwitchPreference
android:defaultValue="true"
android:enabled="true"
android:key="prefLockscreenBackground"

View File

@ -37,12 +37,12 @@ subprojects {
}
project.ext {
compileSdkVersion = 25
compileSdkVersion = 26
buildToolsVersion = "27.0.3"
minSdkVersion = 14
targetSdkVersion = 26
supportVersion = "25.3.1"
supportVersion = "26.1.0"
commonsioVersion = "2.5"
commonslangVersion = "3.6"
commonstextVersion = "1.3"
@ -67,7 +67,7 @@ project.ext {
castCompanionLibVer = "2.9.1"
playServicesVersion = "8.4.0"
wearableSupportVersion = "2.0.3"
wearableSupportVersion = "2.2.0"
}
task wrapper(type: Wrapper) {

View File

@ -1,23 +0,0 @@
general:
artifacts:
- app/build/outputs/apk
machine:
environment:
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"'
_JAVA_OPTIONS: "-Xms256m -Xmx1280m -XX:MaxPermSize=350m"
java:
version: oraclejdk8
dependencies:
cache_directories:
- ~/.android
- ~/android
pre:
- echo y | android update sdk --no-ui --all --filter "tool,extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository,android-25"
- echo y | android update sdk --no-ui --all --filter "build-tools-27.0.3"
override:
- echo override dependencies
test:
override:
- ./gradlew assembleDebug -PdisablePreDex:
timeout: 1800

View File

@ -49,6 +49,8 @@ repositories {
dependencies {
implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:percent:$supportVersion"
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
implementation "org.apache.commons:commons-text:$commonstextVersion"
implementation ("org.shredzone.flattr4j:flattr4j-core:$flattr4jVersion") {

View File

@ -27,6 +27,7 @@
</service>
<service
android:name=".service.GpodnetSyncService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true" />
<receiver
@ -52,9 +53,17 @@
</intent-filter>
</receiver>
<receiver android:name=".receiver.FeedUpdateReceiver">
<receiver android:name=".receiver.FeedUpdateReceiver"
android:label="@string/feed_update_receiver_name"
android:exported="true"> <!-- allow feeds update to be triggered by external apps -->
</receiver>
<service
android:name=".service.FeedUpdateJobService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
</application>
</manifest>

View File

@ -10,6 +10,7 @@ import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import org.shredzone.flattr4j.exception.FlattrException;
import java.util.LinkedList;
@ -175,7 +176,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0);
Notification notification = new NotificationCompat.Builder(context)
Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR)
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg)))
.setContentIntent(contentIntent)
.setContentTitle(context.getString(R.string.no_flattr_token_title))
@ -208,7 +209,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke
+ context.getString(R.string.flattr_click_failure_count, failed);
}
Notification notification = new NotificationCompat.Builder(context)
Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_ERROR)
.setStyle(new NotificationCompat.BigTextStyle().bigText(subtext))
.setContentIntent(contentIntent)
.setContentTitle(title)

View File

@ -0,0 +1,13 @@
package de.danoeh.antennapod.core.event;
public class ServiceEvent {
public enum Action {
SERVICE_STARTED
}
public final Action action;
public ServiceEvent(Action action) {
this.action = action;
}
}

View File

@ -554,15 +554,9 @@ public class FeedMedia extends FeedFile implements Playable {
public Callable<String> loadShownotes() {
return () -> {
if (item == null) {
item = DBReader.getFeedItem(
itemID);
item = DBReader.getFeedItem(itemID);
}
if (item.getContentEncoded() == null || item.getDescription() == null) {
DBReader.loadExtraInformationOfFeedItem(
item);
}
return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription();
return item.loadShownotes().call();
};
}

View File

@ -1,18 +1,21 @@
package de.danoeh.antennapod.core.preferences;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import org.json.JSONArray;
import org.json.JSONException;
@ -21,19 +24,9 @@ import java.io.IOException;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
import de.danoeh.antennapod.core.util.Converter;
/**
* Provides access to preferences set by the user in the settings screen. A
* private instance of this class must first be instantiated via
@ -783,60 +776,17 @@ public class UserPreferences {
int[] timeOfDay = getUpdateTimeOfDay();
Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay));
if (timeOfDay.length == 2) {
restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
AutoUpdateManager.restartUpdateTimeOfDayAlarm(context, timeOfDay[0], timeOfDay[1]);
} else {
long milliseconds = getUpdateInterval();
long startTrigger = milliseconds;
if (now) {
startTrigger = TimeUnit.SECONDS.toMillis(10);
}
restartUpdateIntervalAlarm(startTrigger, milliseconds);
AutoUpdateManager.restartUpdateIntervalAlarm(context, startTrigger, milliseconds);
}
}
/**
* Sets the interval in which the feeds are refreshed automatically
*/
private static void restartUpdateIntervalAlarm(long triggerAtMillis, long intervalMillis) {
Log.d(TAG, "Restarting update alarm.");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, FeedUpdateReceiver.class);
PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
alarmManager.cancel(updateIntent);
if (intervalMillis > 0) {
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + triggerAtMillis,
updateIntent);
Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h");
} else {
Log.d(TAG, "Automatic update was deactivated");
}
}
/**
* Sets time of day the feeds are refreshed automatically
*/
private static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) {
Log.d(TAG, "Restarting update alarm.");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0,
new Intent(context, FeedUpdateReceiver.class), 0);
alarmManager.cancel(updateIntent);
Calendar now = Calendar.getInstance();
Calendar alarm = (Calendar)now.clone();
alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay);
alarm.set(Calendar.MINUTE, minute);
if (alarm.before(now) || alarm.equals(now)) {
alarm.add(Calendar.DATE, 1);
}
Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis());
alarmManager.set(AlarmManager.RTC_WAKEUP,
alarm.getTimeInMillis(),
updateIntent);
Log.d(TAG, "Changed alarm to new time of day " + hoursOfDay + ":" + minute);
}
/**
* Reads episode cache size as it is saved in the episode_cache_size_values array.
*/

View File

@ -7,8 +7,7 @@ import android.util.Log;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.FeedUpdateUtils;
/**
* Refreshes all feeds when it receives an intent
@ -21,11 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received intent");
ClientConfig.initialize(context);
if (NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()) {
DBTasks.refreshAllFeeds(context, null);
} else {
Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
}
FeedUpdateUtils.startAutoUpdate(context, null);
UserPreferences.restartUpdateAlarm(false);
}

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.KeyEvent;
@ -29,7 +30,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
Intent serviceIntent = new Intent(context, PlaybackService.class);
serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode());
serviceIntent.putExtra(EXTRA_SOURCE, event.getSource());
context.startService(serviceIntent);
ContextCompat.startForegroundService(context, serviceIntent);
}
}

View File

@ -1,17 +1,15 @@
package de.danoeh.antennapod.receiver;
package de.danoeh.antennapod.core.receiver;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import java.util.Arrays;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.service.PlayerWidgetService;
public class PlayerWidget extends AppWidgetProvider {
private static final String TAG = "PlayerWidget";
@ -22,17 +20,7 @@ public class PlayerWidget extends AppWidgetProvider {
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
super.onReceive(context, intent);
// don't do anything if we're not enabled
if (!isEnabled(context)) {
return;
}
// these come from the PlaybackService when things should get updated
if (TextUtils.equals(intent.getAction(), PlaybackService.FORCE_WIDGET_UPDATE)) {
startUpdate(context);
} else if (TextUtils.equals(intent.getAction(), PlaybackService.STOP_WIDGET_UPDATE)) {
stopUpdate(context);
}
PlayerWidgetJobService.updateWidget(context);
}
@Override
@ -40,14 +28,13 @@ public class PlayerWidget extends AppWidgetProvider {
super.onEnabled(context);
Log.d(TAG, "Widget enabled");
setEnabled(context, true);
startUpdate(context);
PlayerWidgetJobService.updateWidget(context);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
startUpdate(context);
PlayerWidgetJobService.updateWidget(context);
}
@Override
@ -55,20 +42,9 @@ public class PlayerWidget extends AppWidgetProvider {
super.onDisabled(context);
Log.d(TAG, "Widget disabled");
setEnabled(context, false);
stopUpdate(context);
}
private void startUpdate(Context context) {
Log.d(TAG, "startUpdate() called with: " + "context = [" + context + "]");
context.startService(new Intent(context, PlayerWidgetService.class));
}
private void stopUpdate(Context context) {
Log.d(TAG, "stopUpdate() called with: " + "context = [" + context + "]");
context.stopService(new Intent(context, PlayerWidgetService.class));
}
private boolean isEnabled(Context context) {
public static boolean isEnabled(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getBoolean(KEY_ENABLED, false);
}

View File

@ -0,0 +1,34 @@
package de.danoeh.antennapod.core.service;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.FeedUpdateUtils;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class FeedUpdateJobService extends JobService {
private static final String TAG = "FeedUpdateJobService";
@Override
public boolean onStartJob(JobParameters params) {
Log.d(TAG, "Job started");
ClientConfig.initialize(getApplicationContext());
FeedUpdateUtils.startAutoUpdate(getApplicationContext(), () -> {
UserPreferences.restartUpdateAlarm(false);
jobFinished(params, false); // needsReschedule = false
});
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
return true;
}
}

View File

@ -3,10 +3,10 @@ package de.danoeh.antennapod.core.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
import android.support.v4.util.ArrayMap;
import android.util.Log;
@ -15,6 +15,7 @@ import android.util.Pair;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
@ -37,12 +38,13 @@ 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.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
/**
* Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument.
* This class also provides static methods for starting the GpodnetSyncService.
*/
public class GpodnetSyncService extends Service {
public class GpodnetSyncService extends JobIntentService {
private static final String TAG = "GpodnetSyncService";
private static final long WAIT_INTERVAL = 5000L;
@ -55,12 +57,17 @@ public class GpodnetSyncService extends Service {
private GpodnetService service;
private boolean syncSubscriptions = false;
private boolean syncActions = false;
private static final AtomicInteger syncActionCount = new AtomicInteger(0);
private static boolean syncSubscriptions = false;
private static boolean syncActions = false;
private static void enqueueWork(Context context, Intent intent) {
enqueueWork(context, GpodnetSyncService.class, 0, intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
protected void onHandleWork(@NonNull Intent intent) {
final String action = intent.getStringExtra(ARG_ACTION);
if (action != null) {
switch(action) {
case ACTION_SYNC:
@ -78,24 +85,20 @@ public class GpodnetSyncService extends Service {
}
if(syncSubscriptions || syncActions) {
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
syncWaiterThread.restart();
int syncActionId = syncActionCount.incrementAndGet();
try {
Thread.sleep(WAIT_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (syncActionId == syncActionCount.get()) {
// onHandleWork was not called again in the meantime
sync();
}
}
} else {
Log.e(TAG, "Received invalid intent: action argument is null");
}
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
syncWaiterThread.interrupt();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private synchronized GpodnetService tryLogin() throws GpodnetServiceException {
@ -109,6 +112,7 @@ public class GpodnetSyncService extends Service {
private synchronized void sync() {
if (!GpodnetPreferences.loggedIn() || !NetworkUtils.networkAvailable()) {
stopForeground(true);
stopSelf();
return;
}
@ -125,7 +129,6 @@ public class GpodnetSyncService extends Service {
}
syncActions = false;
}
stopSelf();
}
private synchronized void syncSubscriptionChanges() {
@ -319,7 +322,7 @@ public class GpodnetSyncService extends Service {
}
PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this);
Notification notification = new NotificationCompat.Builder(this)
Notification notification = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setContentTitle(title)
.setContentText(description)
.setContentIntent(activityIntent)
@ -331,69 +334,11 @@ public class GpodnetSyncService extends Service {
nm.notify(id, notification);
}
private final WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
@Override
public void onWaitCompleted() {
sync();
}
};
private abstract class WaiterThread {
private final long waitInterval;
private Thread thread;
private WaiterThread(long waitInterval) {
this.waitInterval = waitInterval;
reinit();
}
public abstract void onWaitCompleted();
public void exec() {
if (!thread.isAlive()) {
thread.start();
}
}
private void reinit() {
if (thread != null && thread.isAlive()) {
Log.d(TAG, "Interrupting waiter thread");
thread.interrupt();
}
thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(waitInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!isInterrupted()) {
synchronized (this) {
onWaitCompleted();
}
}
}
};
}
public void restart() {
reinit();
exec();
}
public void interrupt() {
if (thread != null && thread.isAlive()) {
thread.interrupt();
}
}
}
public static void sendSyncIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC);
context.startService(intent);
enqueueWork(context, intent);
}
}
@ -401,7 +346,7 @@ public class GpodnetSyncService extends Service {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS);
context.startService(intent);
enqueueWork(context, intent);
}
}
@ -409,7 +354,7 @@ public class GpodnetSyncService extends Service {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS);
context.startService(intent);
enqueueWork(context, intent);
}
}
}

View File

@ -0,0 +1,176 @@
package de.danoeh.antennapod.core.service;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.RemoteViews;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
/**
* Updates the state of the player widget
*/
public class PlayerWidgetJobService extends JobIntentService {
private static final String TAG = "PlayerWidgetJobService";
private PlaybackService playbackService;
private final Object waitForService = new Object();
public static void updateWidget(Context context) {
enqueueWork(context, PlayerWidgetJobService.class, 0, new Intent(context, PlayerWidgetJobService.class));
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
if (!PlayerWidget.isEnabled(getApplicationContext())) {
return;
}
if (PlaybackService.isRunning && playbackService == null) {
synchronized (waitForService) {
bindService(new Intent(this, PlaybackService.class), mConnection, 0);
while (playbackService == null) {
try {
waitForService.wait();
} catch (InterruptedException e) {
return;
}
}
}
}
updateViews();
if (playbackService != null) {
try {
unbindService(mConnection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "IllegalArgumentException when trying to unbind service");
}
}
}
private void updateViews() {
ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget);
PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this), 0);
final PendingIntent startAppPending = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
boolean nothingPlaying = false;
Playable media;
PlayerStatus status;
if (playbackService != null) {
media = playbackService.getPlayable();
status = playbackService.getStatus();
} else {
media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
status = PlayerStatus.STOPPED;
}
if (media != null) {
views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
String progressString;
if (playbackService != null) {
progressString = getProgressString(playbackService.getCurrentPosition(), playbackService.getDuration());
} else {
progressString = getProgressString(media.getPosition(), media.getDuration());
}
if (progressString != null) {
views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
views.setTextViewText(R.id.txtvProgress, progressString);
}
if (status == PlayerStatus.PLAYING) {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp);
if (Build.VERSION.SDK_INT >= 15) {
views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
}
} else {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
if (Build.VERSION.SDK_INT >= 15) {
views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
}
}
views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent());
} else {
nothingPlaying = true;
}
if (nothingPlaying) {
// start the app if they click anything
views.setOnClickPendingIntent(R.id.layout_left, startAppPending);
views.setOnClickPendingIntent(R.id.butPlay, startAppPending);
views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
views.setTextViewText(R.id.txtvTitle,
this.getString(R.string.no_media_playing_label));
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
}
manager.updateAppWidget(playerWidget, views);
}
/**
* Creates an intent which fakes a mediabutton press
*/
private PendingIntent createMediaButtonIntent() {
KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class);
startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
}
private String getProgressString(int position, int duration) {
if (position > 0 && duration > 0) {
return Converter.getDurationStringLong(position) + " / "
+ Converter.getDurationStringLong(duration);
} else {
return null;
}
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Connection to service established");
if (service instanceof PlaybackService.LocalBinder) {
synchronized (waitForService) {
playbackService = ((PlaybackService.LocalBinder) service).getService();
waitForService.notifyAll();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
playbackService = null;
Log.d(TAG, "Disconnected from service");
}
};
}

View File

@ -7,8 +7,6 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.os.Binder;
import android.os.Build;
@ -22,6 +20,7 @@ import android.util.Log;
import android.util.Pair;
import android.webkit.URLUtil;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import org.apache.commons.io.FileUtils;
import org.xml.sax.SAXException;
@ -295,6 +294,7 @@ public class DownloadService extends Service {
setupNotificationBuilders();
requester = DownloadRequester.getInstance();
startForeground(NOTIFICATION_ID, updateNotifications());
}
@Override
@ -339,7 +339,7 @@ public class DownloadService extends Service {
}
private void setupNotificationBuilders() {
notificationCompatBuilder = new NotificationCompat.Builder(this)
notificationCompatBuilder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOADING)
.setOngoing(true)
.setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this))
.setSmallIcon(R.drawable.stat_notify_sync);
@ -352,7 +352,7 @@ public class DownloadService extends Service {
/**
* Updates the contents of the service's notifications. Should be called
* before setupNotificationBuilders.
* after setupNotificationBuilders.
*/
private Notification updateNotifications() {
if (notificationCompatBuilder == null) {
@ -499,7 +499,7 @@ public class DownloadService extends Service {
if (createReport) {
Log.d(TAG, "Creating report");
// create notification object
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR)
.setTicker(getString(R.string.download_report_title))
.setContentTitle(getString(R.string.download_report_content_title))
.setContentText(
@ -551,7 +551,7 @@ public class DownloadService extends Service {
final String resourceTitle = (downloadRequest.getTitle() != null) ?
downloadRequest.getTitle() : downloadRequest.getSource();
NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this);
NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this, NotificationUtils.CHANNEL_ID_USER_ACTION);
builder.setTicker(getText(R.string.authentication_notification_title))
.setContentTitle(getText(R.string.authentication_notification_title))
.setContentText(getText(R.string.authentication_notification_msg))

View File

@ -613,6 +613,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
public void shutdown() {
executor.shutdown();
if (mediaPlayer != null) {
try {
mediaPlayer.stop();
} catch (Exception ignore) { }
mediaPlayer.release();
}
releaseWifiLockIfNecessary();

View File

@ -30,12 +30,10 @@ import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.view.InputDeviceCompat;
import android.support.v7.app.NotificationCompat;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.widget.Toast;
@ -49,6 +47,7 @@ import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
@ -60,11 +59,13 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
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.FeedSearcher;
import de.danoeh.antennapod.core.util.IntList;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
@ -74,8 +75,6 @@ import de.greenrobot.event.EventBus;
* Controls the MediaPlayer that plays a FeedMedia-file
*/
public class PlaybackService extends MediaBrowserServiceCompat {
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
*/
@ -313,6 +312,31 @@ public class PlaybackService extends MediaBrowserServiceCompat {
flavorHelper.initializeMediaPlayer(PlaybackService.this);
mediaSession.setActive(true);
NotificationCompat.Builder notificationBuilder = createBasicNotification();
startForeground(NOTIFICATION_ID, notificationBuilder.build());
EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED));
setupNotification(Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()));
}
private NotificationCompat.Builder createBasicNotification() {
final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Builder(
this, NotificationUtils.CHANNEL_ID_PLAYING)
.setContentTitle(getString(R.string.app_name))
.setContentText("Service is running") // Just in case the notification is not updated (should not occur)
.setOngoing(false)
.setContentIntent(pIntent)
.setWhen(0) // we don't need the time
.setSmallIcon(smallIcon)
.setPriority(NotificationCompat.PRIORITY_MIN);
}
@Override
@ -447,8 +471,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (keycode != -1) {
Log.d(TAG, "Received media button event");
handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE,
InputDeviceCompat.SOURCE_CLASS_NONE));
handleKeycode(keycode, true);
} else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
started = true;
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
@ -472,7 +495,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Handles media button events
* return: keycode was handled
*/
private boolean handleKeycode(int keycode, int source) {
private boolean handleKeycode(int keycode, boolean notificationButton) {
Log.d(TAG, "Handling keycode: " + keycode);
final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
final PlayerStatus status = info.playerStatus;
@ -488,6 +511,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} else if (status == PlayerStatus.INITIALIZED) {
mediaPlayer.setStartWhenPrepared(true);
mediaPlayer.prepare();
} else if (mediaPlayer.getPlayable() == null) {
startPlayingFromPreferences();
}
return true;
case KeyEvent.KEYCODE_MEDIA_PLAY:
@ -496,6 +521,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} else if (status == PlayerStatus.INITIALIZED) {
mediaPlayer.setStartWhenPrepared(true);
mediaPlayer.prepare();
} else if (mediaPlayer.getPlayable() == null) {
startPlayingFromPreferences();
}
return true;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
@ -505,7 +532,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return true;
case KeyEvent.KEYCODE_MEDIA_NEXT:
if (source == InputDevice.SOURCE_CLASS_NONE ||
if (notificationButton ||
UserPreferences.shouldHardwareButtonSkip()) {
// assume the skip command comes from a notification or the lockscreen
// a >| skip button should actually skip
@ -549,6 +576,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return false;
}
private void startPlayingFromPreferences() {
Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
if (playable != null) {
mediaPlayer.playMediaObject(playable, false, true, true);
started = true;
PlaybackService.this.updateMediaSessionMetadata(playable);
}
}
/**
* Called by a mediaplayer Activity as soon as it has prepared its
* mediaplayer.
@ -567,8 +603,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
public void notifyVideoSurfaceAbandoned() {
stopForeground(!UserPreferences.isPersistNotify());
mediaPlayer.pause(true, false);
mediaPlayer.resetVideoSurface();
setupNotification(getPlayable());
stopForeground(!UserPreferences.isPersistNotify());
}
private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
@ -602,7 +640,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void onWidgetUpdaterTick() {
updateWidget();
PlayerWidgetJobService.updateWidget(getBaseContext());
}
@Override
@ -664,7 +702,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED);
// statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
sendBroadcast(statusUpdate);
updateWidget();
PlayerWidgetJobService.updateWidget(getBaseContext());
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
}
@ -807,7 +845,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (!isCasting) {
stopForeground(true);
}
stopWidgetUpdater();
}
if (mediaType == null) {
sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
@ -1171,10 +1208,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* 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);
setupNotification(info.playable);
}
private synchronized void setupNotification(final Playable playable) {
if (notificationSetupThread != null) {
notificationSetupThread.interrupt();
}
@ -1184,12 +1221,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void run() {
Log.d(TAG, "Starting background work");
if (info.playable != null) {
if (playable != null) {
int iconSize = getResources().getDimensionPixelSize(
android.R.dimen.notification_large_icon_width);
try {
icon = Glide.with(PlaybackService.this)
.load(info.playable.getImageLocation())
.load(playable.getImageLocation())
.asBitmap()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.centerCrop()
@ -1208,24 +1245,18 @@ public class PlaybackService extends MediaBrowserServiceCompat {
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();
if (!Thread.currentThread().isInterrupted() && started && playable != null) {
String contentText = playable.getEpisodeTitle();
String contentTitle = 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)
NotificationCompat.Builder notificationBuilder = createBasicNotification();
notificationBuilder.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
.setPriority(UserPreferences.getNotifyPriority())
.setLargeIcon(icon); // set notification priority
IntList compactActionList = new IntList();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
@ -1293,7 +1324,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_STOP, numActions);
notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle()
notificationBuilder.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(compactActionList.toArray())
.setShowCancelButton(true)
@ -1360,16 +1391,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
}
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();
}
@ -1762,11 +1783,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
public boolean onMediaButtonEvent(final Intent mediaButton) {
Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")");
if (mediaButton != null) {
KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT);
KeyEvent keyEvent = (KeyEvent) mediaButton.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getRepeatCount() == 0) {
return handleKeycode(keyEvent.getKeyCode(), keyEvent.getSource());
return handleKeycode(keyEvent.getKeyCode(), false);
}
}
return false;

View File

@ -4,6 +4,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.ArrayList;
@ -38,6 +40,7 @@ import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import static android.content.Context.MODE_PRIVATE;
@ -123,16 +126,13 @@ public final class DBTasks {
media);
}
}
// Start playback Service
Intent launchIntent = new Intent(context, PlaybackService.class);
launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
startWhenPrepared);
launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
shouldStream);
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
true);
context.startService(launchIntent);
new PlaybackServiceStarter(context, media)
.callEvenIfRunning(true)
.startWhenPrepared(startWhenPrepared)
.shouldStream(shouldStream)
.start();
if (showPlayer) {
// Launch media player
context.startActivity(PlaybackService.getPlayerActivityIntent(
@ -155,42 +155,56 @@ public final class DBTasks {
* Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
* enqueuing Feeds for download from a previous call
*
* @param context Might be used for accessing the database
* @param feeds List of Feeds that should be refreshed.
* @param context Might be used for accessing the database
* @param feeds List of Feeds that should be refreshed.
*/
public static void refreshAllFeeds(final Context context,
final List<Feed> feeds) {
if (isRefreshing.compareAndSet(false, true)) {
new Thread() {
public void run() {
if (feeds != null) {
refreshFeeds(context, feeds);
} else {
refreshFeeds(context, DBReader.getFeedList());
}
isRefreshing.set(false);
public static void refreshAllFeeds(final Context context, final List<Feed> feeds) {
refreshAllFeeds(context, feeds, null);
}
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
if (FlattrUtils.hasToken()) {
Log.d(TAG, "Flattring all pending things.");
new FlattrClickWorker(context).executeAsync(); // flattr pending things
Log.d(TAG, "Fetching flattr status.");
new FlattrStatusFetcher(context).start();
}
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
Log.d(TAG, "refreshAllFeeds autodownload");
autodownloadUndownloadedItems(context);
}
}.start();
} else {
/**
* Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
* enqueuing Feeds for download from a previous call
*
* @param context Might be used for accessing the database
* @param feeds List of Feeds that should be refreshed.
* @param callback Called after everything was added enqueued for download. Might be null.
*/
public static void refreshAllFeeds(final Context context, final List<Feed> feeds, @Nullable Runnable callback) {
if (!isRefreshing.compareAndSet(false, true)) {
Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked");
return;
}
new Thread(() -> {
if (feeds != null) {
refreshFeeds(context, feeds);
} else {
refreshFeeds(context, DBReader.getFeedList());
}
isRefreshing.set(false);
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
if (FlattrUtils.hasToken()) {
Log.d(TAG, "Flattring all pending things.");
new FlattrClickWorker(context).executeAsync(); // flattr pending things
Log.d(TAG, "Fetching flattr status.");
new FlattrStatusFetcher(context).start();
}
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
Log.d(TAG, "refreshAllFeeds autodownload");
autodownloadUndownloadedItems(context);
if (callback != null) {
callback.run();
}
}).start();
}
/**

View File

@ -43,6 +43,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.Permutor;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import de.danoeh.antennapod.core.util.flattr.FlattrThing;
import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing;
@ -187,6 +188,9 @@ public class DBWriter {
if(queue.remove(item)) {
removed.add(item);
}
if (item.getState() == FeedItem.State.PLAYING && PlaybackService.isRunning) {
context.stopService(new Intent(context, PlaybackService.class));
}
if (item.getMedia() != null
&& item.getMedia().isDownloaded()) {
File mediaFile = new File(item.getMedia()
@ -991,6 +995,32 @@ public class DBWriter {
});
}
/**
* Similar to sortQueue, but allows more complex reordering by providing whole-queue context.
* @param permutor Encapsulates whole-Queue reordering logic.
* @param broadcastUpdate <code>true</code> if this operation should trigger a
* QueueUpdateBroadcast. This option should be set to <code>false</code>
* if the caller wants to avoid unexpected updates of the GUI.
*/
public static Future<?> reorderQueue(final Permutor<FeedItem> permutor, final boolean broadcastUpdate) {
return dbExec.submit(() -> {
final PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
final List<FeedItem> queue = DBReader.getQueue(adapter);
if (queue != null) {
permutor.reorder(queue);
adapter.setQueue(queue);
if (broadcastUpdate) {
EventBus.getDefault().post(QueueEvent.sorted(queue));
}
} else {
Log.e(TAG, "reorderQueue: Could not load queue");
}
adapter.close();
});
}
/**
* Sets the 'auto_download'-attribute of specific FeedItem.
*

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
@ -81,7 +82,7 @@ public class DownloadRequester {
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
context.startService(launchIntent);
ContextCompat.startForegroundService(context, launchIntent);
return true;
}
@ -176,8 +177,8 @@ public class DownloadRequester {
args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr());
args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages);
download(context, feed, null, new File(getFeedfilePath(context),
getFeedfileName(feed)), true, username, password, lastModified, true, args);
download(context, feed, null, new File(getFeedfilePath(), getFeedfileName(feed)),
true, username, password, lastModified, true, args);
}
}
@ -203,8 +204,7 @@ public class DownloadRequester {
if (feedmedia.getFile_url() != null) {
dest = new File(feedmedia.getFile_url());
} else {
dest = new File(getMediafilePath(context, feedmedia),
getMediafilename(feedmedia));
dest = new File(getMediafilePath(feedmedia), getMediafilename(feedmedia));
}
download(context, feedmedia, feed,
dest, false, username, password, null, false, null);
@ -305,10 +305,8 @@ public class DownloadRequester {
return downloads.size();
}
private synchronized String getFeedfilePath(Context context)
throws DownloadRequestException {
return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH)
.toString() + "/";
private synchronized String getFeedfilePath() throws DownloadRequestException {
return getExternalFilesDirOrThrowException(FEED_DOWNLOADPATH).toString() + "/";
}
private synchronized String getFeedfileName(Feed feed) {
@ -319,10 +317,8 @@ public class DownloadRequester {
return "feed-" + FileNameGenerator.generateFileName(filename);
}
private synchronized String getMediafilePath(Context context, FeedMedia media)
throws DownloadRequestException {
private synchronized String getMediafilePath(FeedMedia media) throws DownloadRequestException {
File externalStorage = getExternalFilesDirOrThrowException(
context,
MEDIA_DOWNLOADPATH
+ FileNameGenerator.generateFileName(media.getItem()
.getFeed().getTitle()) + "/"
@ -330,8 +326,7 @@ public class DownloadRequester {
return externalStorage.toString();
}
private File getExternalFilesDirOrThrowException(Context context,
String type) throws DownloadRequestException {
private File getExternalFilesDirOrThrowException(String type) throws DownloadRequestException {
File result = UserPreferences.getDataFolder(type);
if (result == null) {
throw new DownloadRequestException(

View File

@ -75,4 +75,18 @@ public class FeedItemUtil {
return false;
}
/**
* Get the link for the feed item for the purpose of Share. It fallbacks to
* use the feed's link if the named feed item has no link.
*/
public static String getLinkWithFallback(FeedItem item) {
if (item == null) {
return null;
} else if (item.getLink() != null) {
return item.getLink();
} else if (item.getFeed() != null) {
return item.getFeed().getLink();
}
return null;
}
}

View File

@ -0,0 +1,21 @@
package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.util.Log;
import de.danoeh.antennapod.core.storage.DBTasks;
public class FeedUpdateUtils {
private static final String TAG = "FeedUpdateUtils";
private FeedUpdateUtils() {
}
public static void startAutoUpdate(Context context, Runnable callback) {
if (NetworkUtils.networkAvailable() && NetworkUtils.isDownloadAllowed()) {
DBTasks.refreshAllFeeds(context, null, callback);
} else {
Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
}
}
}

View File

@ -0,0 +1,17 @@
package de.danoeh.antennapod.core.util;
import java.util.List;
/**
* Interface for passing around list permutor method. This is used for cases where a simple comparator
* won't work (e.g. Random, Smart Shuffle, etc).
*
* @param <E> the type of elements in the list
*/
public interface Permutor<E> {
/**
* Reorders the specified list.
* @param queue A (modifiable) list of elements to be reordered
*/
void reorder(List<E> queue);
}

View File

@ -2,7 +2,12 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@ -20,11 +25,15 @@ public class QueueSorter {
DURATION_ASC,
DURATION_DESC,
FEED_TITLE_ASC,
FEED_TITLE_DESC
FEED_TITLE_DESC,
RANDOM,
SMART_SHUFFLE_ASC,
SMART_SHUFFLE_DESC
}
public static void sort(final Context context, final Rule rule, final boolean broadcastUpdate) {
Comparator<FeedItem> comparator = null;
Permutor<FeedItem> permutor = null;
switch (rule) {
case EPISODE_TITLE_ASC:
@ -68,11 +77,109 @@ public class QueueSorter {
case FEED_TITLE_DESC:
comparator = (f1, f2) -> f2.getFeed().getTitle().compareTo(f1.getFeed().getTitle());
break;
case RANDOM:
permutor = Collections::shuffle;
break;
case SMART_SHUFFLE_ASC:
permutor = (queue) -> smartShuffle(queue, true);
break;
case SMART_SHUFFLE_DESC:
permutor = (queue) -> smartShuffle(queue, false);
break;
default:
}
if (comparator != null) {
DBWriter.sortQueue(comparator, broadcastUpdate);
} else if (permutor != null) {
DBWriter.reorderQueue(permutor, broadcastUpdate);
}
}
/**
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in
* the queue.
*
* A listener might want to hear episodes from any given feed in pubdate order, but would
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
*
* The Smart Shuffle algorithm involves choosing episodes (in round-robin fashion) from a
* collection of individual, pubdate-sorted lists that each contain only items from a specific
* feed.
*
* Of course, clusters of consecutive episodes <i>at the end of the queue</i> may be
* unavoidable. This seems unlikely to be an issue for most users who presumably maintain
* large queues with new episodes continuously being added.
*
* For example, given a queue containing three episodes each from three different feeds
* (A, B, and C), a simple pubdate sort might result in a queue that looks like the following:
*
* B1, B2, B3, A1, A2, C1, C2, C3, A3
*
* (note that feed B episodes were all published before the first feed A episode, so a simple
* pubdate sort will often result in significant clustering of episodes from a single feed)
*
* Using Smart Shuffle, the resulting queue would look like the following:
*
* A1, B1, C1, A2, B2, C2, A3, B3, C3
*
* (note that episodes above <i>aren't strictly ordered in terms of pubdate</i>, but episodes
* of each feed <b>do</b> appear in pubdate order)
*
* @param queue A (modifiable) list of FeedItem elements to be reordered.
* @param ascending {@code true} to use ascending pubdate in the reordering;
* {@code false} for descending.
*/
private static void smartShuffle(List<FeedItem> queue, boolean ascending) {
// Divide FeedItems into lists by feed
Map<Long, List<FeedItem>> map = new HashMap<>();
while (!queue.isEmpty()) {
FeedItem item = queue.remove(0);
Long id = item.getFeedId();
if (!map.containsKey(id)) {
map.put(id, new ArrayList<>());
}
map.get(id).add(item);
}
// Sort each individual list by PubDate (ascending/descending)
Comparator<FeedItem> itemComparator = ascending
? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
: (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
for (Long id : map.keySet()) {
Collections.sort(map.get(id), itemComparator);
}
// Create a list of the individual FeedItems lists, and sort it by feed title (ascending).
// Doing this ensures that the feed order we use is predictable/deterministic.
List<List<FeedItem>> feeds = new ArrayList<>(map.values());
Collections.sort(feeds,
// (we use a desc sort here, since we're iterating back-to-front below)
(f1, f2) -> f2.get(0).getFeed().getTitle().compareTo(f1.get(0).getFeed().getTitle()));
// Cycle through the (sorted) feed lists in a round-robin fashion, removing the first item
// and adding it back into to the original queue
while (!feeds.isEmpty()) {
// Iterate across the (sorted) list of feeds, removing the first item in each, and
// appending it to the queue. Note that we're iterating back-to-front here, since we
// will be deleting feed lists as they become empty.
for (int i = feeds.size() - 1; i >= 0; --i) {
List<FeedItem> items = feeds.get(i);
queue.add(items.remove(0));
// Removed the last item in this particular feed? Then remove this feed from the
// list of feeds.
if (items.isEmpty()) {
feeds.remove(i);
}
}
}
}
}

View File

@ -50,11 +50,15 @@ public class ShareUtils {
return item.getFeed().getTitle() + ": " + item.getTitle();
}
public static boolean hasLinkToShare(FeedItem item) {
return FeedItemUtil.getLinkWithFallback(item) != null;
}
public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) {
String text = getItemShareText(item) + " " + item.getLink();
String text = getItemShareText(item) + " " + FeedItemUtil.getLinkWithFallback(item);
if(withPosition) {
int pos = item.getMedia().getPosition();
text = item.getLink() + " [" + Converter.getDurationStringLong(pos) + "]";
text += " [" + Converter.getDurationStringLong(pos) + "]";
}
shareLink(context, text);
}

View File

@ -0,0 +1,155 @@
package de.danoeh.antennapod.core.util.download;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.RequiresApi;
import android.util.Log;
import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
import de.danoeh.antennapod.core.service.FeedUpdateJobService;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
public class AutoUpdateManager {
private static final int JOB_ID_FEED_UPDATE = 42;
private static final String TAG = "AutoUpdateManager";
private AutoUpdateManager() {
}
/**
* Sets the interval in which the feeds are refreshed automatically
*/
public static void restartUpdateIntervalAlarm(Context context, long triggerAtMillis, long intervalMillis) {
Log.d(TAG, "Restarting update alarm.");
if (Build.VERSION.SDK_INT >= 24) {
restartJobServiceInterval(context, intervalMillis);
} else {
restartAlarmManagerInterval(context, triggerAtMillis, intervalMillis);
}
}
/**
* Sets time of day the feeds are refreshed automatically
*/
public static void restartUpdateTimeOfDayAlarm(Context context, int hoursOfDay, int minute) {
Log.d(TAG, "Restarting update alarm.");
Calendar now = Calendar.getInstance();
Calendar alarm = (Calendar)now.clone();
alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay);
alarm.set(Calendar.MINUTE, minute);
if (alarm.before(now) || alarm.equals(now)) {
alarm.add(Calendar.DATE, 1);
}
if (Build.VERSION.SDK_INT >= 24) {
long triggerAtMillis = alarm.getTimeInMillis() - now.getTimeInMillis();
restartJobServiceTriggerAt(context, triggerAtMillis);
} else {
restartAlarmManagerTimeOfDay(context, alarm);
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static JobInfo.Builder getFeedUpdateJobBuilder(Context context) {
ComponentName serviceComponent = new ComponentName(context, FeedUpdateJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_FEED_UPDATE, serviceComponent);
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
builder.setPersisted(true);
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.N)
private static void restartJobServiceInterval(Context context, long intervalMillis) {
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler == null) {
Log.d(TAG, "JobScheduler was null.");
return;
}
JobInfo oldJob = jobScheduler.getPendingJob(JOB_ID_FEED_UPDATE);
if (oldJob != null && oldJob.getIntervalMillis() == intervalMillis) {
Log.d(TAG, "JobScheduler was already set at interval " + intervalMillis + ", ignoring.");
return;
}
JobInfo.Builder builder = getFeedUpdateJobBuilder(context);
builder.setPeriodic(intervalMillis);
jobScheduler.cancel(JOB_ID_FEED_UPDATE);
if (intervalMillis <= 0) {
Log.d(TAG, "Automatic update was deactivated");
return;
}
jobScheduler.schedule(builder.build());
Log.d(TAG, "JobScheduler was set at interval " + intervalMillis);
}
private static void restartAlarmManagerInterval(Context context, long triggerAtMillis, long intervalMillis) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
Log.d(TAG, "AlarmManager was null");
return;
}
PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0,
new Intent(context, FeedUpdateReceiver.class), 0);
alarmManager.cancel(updateIntent);
if (intervalMillis <= 0) {
Log.d(TAG, "Automatic update was deactivated");
return;
}
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + triggerAtMillis,
updateIntent);
Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h");
}
@RequiresApi(api = Build.VERSION_CODES.N)
private static void restartJobServiceTriggerAt(Context context, long triggerAtMillis) {
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler == null) {
Log.d(TAG, "JobScheduler was null.");
return;
}
JobInfo.Builder builder = getFeedUpdateJobBuilder(context);
builder.setMinimumLatency(triggerAtMillis);
jobScheduler.cancel(JOB_ID_FEED_UPDATE);
jobScheduler.schedule(builder.build());
Log.d(TAG, "JobScheduler was set for " + triggerAtMillis);
}
private static void restartAlarmManagerTimeOfDay(Context context, Calendar alarm) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
Log.d(TAG, "AlarmManager was null");
return;
}
PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0,
new Intent(context, FeedUpdateReceiver.class), 0);
alarmManager.cancel(updateIntent);
Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis());
alarmManager.set(AlarmManager.RTC_WAKEUP,
alarm.getTimeInMillis(),
updateIntent);
Log.d(TAG, "Changed alarm to new time of day " + alarm.get(Calendar.HOUR_OF_DAY) + ":" + alarm.get(Calendar.MINUTE));
}
}

View File

@ -0,0 +1,62 @@
package de.danoeh.antennapod.core.util.gui;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import de.danoeh.antennapod.core.R;
public class NotificationUtils {
public static final String CHANNEL_ID_USER_ACTION = "user_action";
public static final String CHANNEL_ID_DOWNLOADING = "downloading";
public static final String CHANNEL_ID_PLAYING = "playing";
public static final String CHANNEL_ID_ERROR = "error";
public static void createChannels(Context context) {
if (android.os.Build.VERSION.SDK_INT < 26) {
return;
}
NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (mNotificationManager != null) {
mNotificationManager.createNotificationChannel(createChannelUserAction(context));
mNotificationManager.createNotificationChannel(createChannelDownloading(context));
mNotificationManager.createNotificationChannel(createChannelPlaying(context));
mNotificationManager.createNotificationChannel(createChannelError(context));
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel createChannelUserAction(Context c) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_USER_ACTION,
c.getString(R.string.notification_channel_user_action), NotificationManager.IMPORTANCE_HIGH);
mChannel.setDescription(c.getString(R.string.notification_channel_user_action_description));
return mChannel;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel createChannelDownloading(Context c) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_DOWNLOADING,
c.getString(R.string.notification_channel_downloading), NotificationManager.IMPORTANCE_LOW);
mChannel.setDescription(c.getString(R.string.notification_channel_downloading_description));
return mChannel;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel createChannelPlaying(Context c) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_PLAYING,
c.getString(R.string.notification_channel_playing), NotificationManager.IMPORTANCE_LOW);
mChannel.setDescription(c.getString(R.string.notification_channel_playing_description));
return mChannel;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel createChannelError(Context c) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_ERROR,
c.getString(R.string.notification_channel_error), NotificationManager.IMPORTANCE_HIGH);
mChannel.setDescription(c.getString(R.string.notification_channel_error_description));
return mChannel;
}
}

View File

@ -3,6 +3,8 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.List;
@ -11,6 +13,7 @@ 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.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.ShownotesProvider;
@ -175,6 +178,23 @@ public interface Playable extends Parcelable,
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.
*
* @return The restored Playable object
*/
@Nullable
public static Playable createInstanceFromPreferences(Context context) {
long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia();
if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
return PlayableUtils.createInstanceFromPreferences(context,
(int) currentlyPlayingMedia, prefs);
}
return null;
}
/**
* 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.

View File

@ -14,6 +14,8 @@ import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@ -59,7 +61,7 @@ public abstract class PlaybackController {
private PlaybackService playbackService;
private Playable media;
private PlayerStatus status;
private PlayerStatus status = PlayerStatus.STOPPED;
private final ScheduledThreadPoolExecutor schedExecutor;
private static final int SCHED_EX_POOLSIZE = 1;
@ -69,6 +71,7 @@ public abstract class PlaybackController {
private boolean mediaInfoLoaded = false;
private boolean released = false;
private boolean initialized = false;
private Subscription serviceBinder;
@ -92,10 +95,22 @@ public abstract class PlaybackController {
}
/**
* Creates a new connection to the playbackService. Should be called in the
* activity's onResume() method.
* Creates a new connection to the playbackService.
*/
public void init() {
if (PlaybackService.isRunning) {
initServiceRunning();
} else {
initServiceNotRunning();
}
}
private synchronized void initServiceRunning() {
if (initialized) {
return;
}
initialized = true;
activity.registerReceiver(statusUpdate, new IntentFilter(
PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
@ -167,7 +182,7 @@ public abstract class PlaybackController {
*/
private void bindToService() {
Log.d(TAG, "Trying to connect to service");
if(serviceBinder != null) {
if (serviceBinder != null) {
serviceBinder.unsubscribe();
}
serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
@ -178,7 +193,7 @@ public abstract class PlaybackController {
if (!PlaybackService.started) {
if (intent != null) {
Log.d(TAG, "Calling start service");
activity.startService(intent);
ContextCompat.startForegroundService(activity, intent);
bound = activity.bindService(intent, mConnection, 0);
} else {
status = PlayerStatus.STOPPED;
@ -198,31 +213,24 @@ public abstract class PlaybackController {
* Returns an intent that starts the PlaybackService and plays the last
* played media or null if no last played media could be found.
*/
private Intent getPlayLastPlayedMediaIntent() {
@Nullable private Intent getPlayLastPlayedMediaIntent() {
Log.d(TAG, "Trying to restore last played media");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
activity.getApplicationContext());
long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia();
if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
Playable media = PlayableUtils.createInstanceFromPreferences(activity,
(int) currentlyPlayingMedia, prefs);
if (media != null) {
Intent serviceIntent = new Intent(activity, PlaybackService.class);
serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false);
serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, true);
boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
}
serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
lastIsStream || !fileExists);
return serviceIntent;
}
Playable media = PlayableUtils.createInstanceFromPreferences(activity);
if (media == null) {
Log.d(TAG, "No last played media found");
return null;
}
Log.d(TAG, "No last played media found");
return null;
boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
}
return new PlaybackServiceStarter(activity, media)
.startWhenPrepared(false)
.shouldStream(lastIsStream || !fileExists)
.getIntent();
}
@ -511,7 +519,7 @@ public abstract class PlaybackController {
"PlaybackService has no media object. Trying to restore last played media.");
Intent serviceIntent = getPlayLastPlayedMediaIntent();
if (serviceIntent != null) {
activity.startService(serviceIntent);
ContextCompat.startForegroundService(activity, serviceIntent);
}
}
*/
@ -576,6 +584,10 @@ public abstract class PlaybackController {
public void playPause() {
if (playbackService == null) {
new PlaybackServiceStarter(activity, media)
.startWhenPrepared(true)
.streamIfLastWasStream()
.start();
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
return;
}
@ -609,6 +621,8 @@ public abstract class PlaybackController {
public int getPosition() {
if (playbackService != null) {
return playbackService.getCurrentPosition();
} else if (media != null) {
return media.getPosition();
} else {
return PlaybackService.INVALID_TIME;
}
@ -617,12 +631,17 @@ public abstract class PlaybackController {
public int getDuration() {
if (playbackService != null) {
return playbackService.getDuration();
} else if (media != null) {
return media.getDuration();
} else {
return PlaybackService.INVALID_TIME;
}
}
public Playable getMedia() {
if (media == null) {
media = PlayableUtils.createInstanceFromPreferences(activity);
}
return media;
}
@ -714,8 +733,13 @@ public abstract class PlaybackController {
}
public boolean isPlayingVideoLocally() {
return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO
&& !PlaybackService.isCasting();
if (PlaybackService.isCasting()) {
return false;
} else if (playbackService != null) {
return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
} else {
return getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO;
}
}
public Pair<Integer, Integer> getVideoSize() {
@ -755,6 +779,21 @@ public abstract class PlaybackController {
}
}
private void initServiceNotRunning() {
if (getMedia() == null) {
return;
}
if (getMedia().getMediaType() == MediaType.AUDIO) {
TypedArray res = activity.obtainStyledAttributes(new int[]{
de.danoeh.antennapod.core.R.attr.av_play_big});
getPlayButton().setImageResource(
res.getResourceId(0, de.danoeh.antennapod.core.R.drawable.ic_play_arrow_grey600_36dp));
res.recycle();
} else {
getPlayButton().setImageResource(R.drawable.ic_av_play_circle_outline_80dp);
}
}
/**
* Refreshes the current position of the media file that is playing.
*/

View File

@ -0,0 +1,76 @@
package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.support.v4.content.ContextCompat;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
public class PlaybackServiceStarter {
private final Context context;
private final Playable media;
private boolean startWhenPrepared = false;
private boolean shouldStream = false;
private boolean callEvenIfRunning = false;
private boolean prepareImmediately = true;
public PlaybackServiceStarter(Context context, Playable media) {
this.context = context;
this.media = media;
}
/**
* Default value: false
*/
public PlaybackServiceStarter shouldStream(boolean shouldStream) {
this.shouldStream = shouldStream;
return this;
}
public PlaybackServiceStarter streamIfLastWasStream() {
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
return shouldStream(lastIsStream);
}
/**
* Default value: false
*/
public PlaybackServiceStarter startWhenPrepared(boolean startWhenPrepared) {
this.startWhenPrepared = startWhenPrepared;
return this;
}
/**
* Default value: false
*/
public PlaybackServiceStarter callEvenIfRunning(boolean callEvenIfRunning) {
this.callEvenIfRunning = callEvenIfRunning;
return this;
}
/**
* Default value: true
*/
public PlaybackServiceStarter prepareImmediately(boolean prepareImmediately) {
this.prepareImmediately = prepareImmediately;
return this;
}
public Intent getIntent() {
Intent launchIntent = new Intent(context, PlaybackService.class);
launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media);
launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, startWhenPrepared);
launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream);
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately);
return launchIntent;
}
public void start() {
if (PlaybackService.isRunning && !callEvenIfRunning) {
return;
}
ContextCompat.startForegroundService(context, getIntent());
}
}

View File

@ -6,6 +6,7 @@
<!-- Activitiy and fragment titles -->
<string name="app_name" translate="false">AntennaPod</string>
<string name="provider_authority" translate="false">de.danoeh.antennapod.provider</string>
<string name="feed_update_receiver_name">Update Subscriptions</string>
<string name="feeds_label">Feeds</string>
<string name="statistics_label">Statistics</string>
<string name="add_feed_label">Add Podcast</string>
@ -29,6 +30,7 @@
<string name="free_space_label">%1$s free</string>
<string name="episode_cache_full_title">Episode cache full</string>
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
<string name="synchronizing">Synchronizing…</string>
<!-- Statistics fragment -->
<string name="total_time_listened_to_podcasts">Total time of podcasts played:</string>
@ -260,6 +262,8 @@
<string name="duration">Duration</string>
<string name="episode_title">Episode title</string>
<string name="feed_title">Feed title</string>
<string name="random">Random</string>
<string name="smart_shuffle">Smart Shuffle</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>
@ -709,4 +713,14 @@
<string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
<string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
<string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
<!-- Notification channels -->
<string name="notification_channel_user_action">Action required</string>
<string name="notification_channel_user_action_description">Shown if your action is required, for example if you need to enter a password.</string>
<string name="notification_channel_downloading">Downloading</string>
<string name="notification_channel_downloading_description">Shown while currently downloading.</string>
<string name="notification_channel_playing">Currently playing</string>
<string name="notification_channel_playing_description">Allows to control playback. This is the main notification you see while playing a podcast.</string>
<string name="notification_channel_error">Errors</string>
<string name="notification_channel_error_description">Shown if something went wrong, for example if download or gpodder sync fails.</string>
</resources>

View File

@ -67,6 +67,7 @@
<item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item>
<item type="attr" name="ic_cast_disconnect">@drawable/ic_cast_disconnect_grey600_36dp</item>
<item type="attr" name="master_switch_background">@color/master_switch_background_light</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark">
@ -135,6 +136,7 @@
<item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item>
<item type="attr" name="ic_cast_disconnect">@drawable/ic_cast_disconnect_white_36dp</item>
<item type="attr" name="master_switch_background">@color/master_switch_background_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="Theme.AntennaPod.Light.NoTitle" parent="Theme.Base.AntennaPod.Light.NoTitle">
@ -204,6 +206,7 @@
<item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item>
<item type="attr" name="ic_cast_disconnect">@drawable/ic_cast_disconnect_grey600_36dp</item>
<item type="attr" name="master_switch_background">@color/master_switch_background_light</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="Theme.AntennaPod.Dark.NoTitle" parent="Theme.Base.AntennaPod.Dark.NoTitle">
@ -273,6 +276,7 @@
<item type="attr" name="ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item>
<item type="attr" name="ic_cast_disconnect">@drawable/ic_cast_disconnect_white_36dp</item>
<item type="attr" name="master_switch_background">@color/master_switch_background_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="Theme.AntennaPod.Dark.Splash" parent="Theme.AppCompat.NoActionBar">