Merge branch 'develop' into notification-default-icon

This commit is contained in:
ByteHamster 2019-10-02 16:03:11 +02:00
commit 71a5a00fd0
96 changed files with 1788 additions and 1109 deletions

View File

@ -1,49 +1,77 @@
version: 2
jobs:
test:
test-debug:
docker:
- image: circleci/android:api-28
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:
name: Create temporary release keystore
command: keytool -noprompt -genkey -v -keystore "app/keystore" -alias alias -storepass password -keypass password -keyalg RSA -validity 10 -dname "CN=antennapod.org, OU=dummy, O=dummy, L=dummy, S=dummy, C=US"
- run:
name: Build debug
command: ./gradlew assembleDebug -PdisablePreDex
- run:
name: Execute debug unit tests
command: ./gradlew :core:testPlayDebugUnitTest -PdisablePreDex
- save_cache:
paths:
- ~/.android
- ~/.gradle
- ~/android
key: v1-android-{{ checksum "build.gradle" }}
test-release:
docker:
- image: circleci/android:api-28
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" }}
- v1-android-
- run:
name: Create temporary release keystore
command: keytool -noprompt -genkey -v -keystore "app/keystore" -alias alias -storepass password -keypass password -keyalg RSA -validity 10 -dname "CN=antennapod.org, OU=dummy, O=dummy, L=dummy, S=dummy, C=US"
- run:
name: Build release
command: ./gradlew assembleRelease -PdisablePreDex
- run:
name: Execute unit tests
name: Execute release unit tests
command: ./gradlew :core:testPlayReleaseUnitTest -PdisablePreDex
- save_cache:
paths:
- ~/.android
- ~/.gradle
- ~/android
key: v1-android-{{ checksum "build.gradle" }}
build-androidtest:
docker:
- image: circleci/android:api-28
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" }}
- v1-android-
- run:
name: Build integration tests
command: ./gradlew :app:assemblePlayDebugAndroidTest -PdisablePreDex
- store_artifacts:
path: app/build/outputs/apk
destination: apks
- save_cache:
paths:
- ~/.android
@ -55,10 +83,8 @@ jobs:
docker:
- image: circleci/android:api-28
working_directory: ~/AntennaPod
steps:
- checkout
- run:
name: Checkstyle
command: ./gradlew checkstyle
@ -66,10 +92,12 @@ jobs:
workflows:
version: 2
test:
unit-tests:
jobs:
- test
- test-debug
- test-release
- build-androidtest
checkstyle:
static-analysis:
jobs:
- checkstyle

View File

@ -18,8 +18,8 @@ android {
// "1.2.3-SNAPSHOT" -> 1020300
// "1.2.3-RC4" -> 1020304
// "1.2.3" -> 1020395
versionCode 1070395
versionName "1.7.3"
versionCode 1070396
versionName "1.7.3b"
testApplicationId "de.test.antennapod"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
generatedDensities = []
@ -139,10 +139,6 @@ dependencies {
implementation "com.android.support:recyclerview-v7:$supportVersion"
compileOnly 'com.google.android.wearable:wearable:2.2.0'
// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version"
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
implementation "commons-io:commons-io:$commonsioVersion"
implementation "org.jsoup:jsoup:$jsoupVersion"
@ -173,8 +169,8 @@ dependencies {
implementation 'com.github.mfietz:fyydlin:v0.4.2'
implementation 'com.github.ByteHamster:SearchPreference:v1.3.0'
implementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation 'com.nanohttpd:nanohttpd-webserver:2.1.1'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -0,0 +1,52 @@
package de.danoeh.antennapod.core.service.download;
import android.support.annotation.NonNull;
import de.danoeh.antennapod.core.util.Consumer;
public class StubDownloader extends Downloader {
private final long downloadTime;
@NonNull
private final Consumer<DownloadStatus> onDownloadComplete;
public StubDownloader(@NonNull DownloadRequest request, long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) {
super(request);
this.downloadTime = downloadTime;
this.onDownloadComplete = onDownloadComplete;
}
@Override
protected void download() {
try {
Thread.sleep(downloadTime);
} catch (Throwable t) {
t.printStackTrace();
}
onDownloadComplete.accept(result);
}
@NonNull
@Override
public DownloadRequest getDownloadRequest() {
return super.getDownloadRequest();
}
@NonNull
@Override
public DownloadStatus getResult() {
return super.getResult();
}
@Override
public boolean isFinished() {
return super.isFinished();
}
@Override
public void cancel() {
super.cancel();
result.setCancelled();
}
}

View File

@ -0,0 +1,127 @@
package de.test.antennapod.service.download;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.service.download.StubDownloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Consumer;
import static de.test.antennapod.util.event.FeedItemEventListener.withFeedItemEventListener;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @see HttpDownloaderTest for the test of actual download (and saving the file)
*/
@RunWith(AndroidJUnit4.class)
public class DownloadServiceTest {
private CountDownLatch latch = null;
private Feed testFeed = null;
private FeedMedia testMedia11 = null;
private DownloadService.DownloaderFactory origFactory = null;
@Before
public void setUp() throws Exception {
origFactory = DownloadService.getDownloaderFactory();
testFeed = setUpTestFeeds();
testMedia11 = testFeed.getItemAtIndex(0).getMedia();
}
private Feed setUpTestFeeds() throws Exception {
Feed feed = new Feed("url", null, "Test Feed title 1");
List<FeedItem> items = new ArrayList<>();
feed.setItems(items);
FeedItem item1 = new FeedItem(0, "Item 1-1", "Item 1-1", "url", new Date(), FeedItem.NEW, feed);
items.add(item1);
FeedMedia media1 = new FeedMedia(0, item1, 123, 1, 1, "audio/mp3", null, "http://example.com/episode.mp3", false, null, 0, 0);
item1.setMedia(media1);
DBWriter.setFeedItem(item1).get();
return feed;
}
@After
public void tearDown() throws Exception {
DownloadService.setDownloaderFactory(origFactory);
}
@Test
public void testEventsGeneratedCaseMediaDownloadSuccess() throws Exception {
// create a stub download that returns successful
//
// OPEN: Ideally, I'd like the download time long enough so that multiple in-progress DownloadEvents
// are generated (to simulate typical download), but it'll make download time quite long (1-2 seconds)
// to do so
DownloadService.setDownloaderFactory(new StubDownloaderFactory(50, downloadStatus -> {
downloadStatus.setSuccessful();
}));
withFeedItemEventListener(feedItemEventListener -> {
try {
assertEquals(0, feedItemEventListener.getEvents().size());
assertFalse("The media in test should not yet been downloaded",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
DownloadRequester.getInstance().downloadMedia(InstrumentationRegistry.getTargetContext(),
testMedia11);
Awaitility.await()
.atMost(1000, TimeUnit.MILLISECONDS)
.until(() -> feedItemEventListener.getEvents().size() > 0);
assertTrue("After media download has completed, FeedMedia object in db should indicate so.",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
} catch (ConditionTimeoutException cte) {
fail("The expected FeedItemEvent (for media download complete) has not been posted. "
+ cte.getMessage());
}
});
}
private static class StubDownloaderFactory implements DownloadService.DownloaderFactory {
private final long downloadTime;
@NonNull
private final Consumer<DownloadStatus> onDownloadComplete;
StubDownloaderFactory(long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) {
this.downloadTime = downloadTime;
this.onDownloadComplete = onDownloadComplete;
}
@Nullable
@Override
public Downloader create(@NonNull DownloadRequest request) {
return new StubDownloader(request, downloadTime, onDownloadComplete);
}
}
}

View File

@ -5,6 +5,12 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.LargeTest;
import org.awaitility.Awaitility;
import org.greenrobot.eventbus.EventBus;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -15,14 +21,14 @@ import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.playback.Playable;
import org.greenrobot.eventbus.EventBus;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static de.test.antennapod.util.event.FeedItemEventListener.withFeedItemEventListener;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@ -123,6 +129,43 @@ public class PlaybackServiceTaskManagerTest {
pstm.shutdown();
}
@Test
public void testQueueUpdatedUponDownloadComplete() throws Exception {
final Context c = InstrumentationRegistry.getInstrumentation().getTargetContext();
{ // Setup test data
List<FeedItem> queue = writeTestQueue("a");
FeedItem item = DBReader.getFeedItem(queue.get(0).getId());
FeedMedia media = new FeedMedia(item, "http://example.com/episode.mp3", 12345, "audio/mp3");
item.setMedia(media);
DBWriter.setFeedMedia(media).get();
DBWriter.setFeedItem(item).get();
}
PlaybackServiceTaskManager pstm = new PlaybackServiceTaskManager(c, defaultPSTM);
final FeedItem testItem = pstm.getQueue().get(0);
assertFalse("The item should not yet be downloaded", testItem.getMedia().isDownloaded());
withFeedItemEventListener( feedItemEventListener -> {
// simulate download complete (in DownloadService.MediaHandlerThread)
FeedItem item = DBReader.getFeedItem(testItem.getId());
item.getMedia().setDownloaded(true);
item.getMedia().setFile_url("file://123");
item.setAutoDownload(false);
DBWriter.setFeedMedia(item.getMedia()).get();
DBWriter.setFeedItem(item).get();
Awaitility.await()
.atMost(1000, TimeUnit.MILLISECONDS)
.until(() -> feedItemEventListener.getEvents().size() > 0);
final FeedItem itemUpdated = pstm.getQueue().get(0);
assertTrue("media.isDownloaded() should be true - The queue in PlaybackService should be updated after download is completed",
itemUpdated.getMedia().isDownloaded());
});
pstm.shutdown();
}
@Test
public void testStartPositionSaver() throws InterruptedException {
final Context c = InstrumentationRegistry.getInstrumentation().getTargetContext();

View File

@ -0,0 +1,46 @@
package de.test.antennapod.util.event;
import android.support.annotation.NonNull;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.ArrayList;
import java.util.List;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import io.reactivex.functions.Consumer;
/**
* Test helpers to listen {@link FeedItemEvent} and handle them accordingly
*
*/
public class FeedItemEventListener {
private final List<FeedItemEvent> events = new ArrayList<>();
/**
* Provides an listener subscribing to {@link FeedItemEvent} that the callers can use
*
* Note: it uses RxJava's version of {@link Consumer} because it allows exceptions to be thrown.
*/
public static void withFeedItemEventListener(@NonNull Consumer<FeedItemEventListener> consumer)
throws Exception {
FeedItemEventListener feedItemEventListener = new FeedItemEventListener();
try {
EventBus.getDefault().register(feedItemEventListener);
consumer.accept(feedItemEventListener);
} finally {
EventBus.getDefault().unregister(feedItemEventListener);
}
}
@Subscribe
public void onEvent(FeedItemEvent event) {
events.add(event);
}
@NonNull
public List<? extends FeedItemEvent> getEvents() {
return events;
}
}

View File

@ -105,17 +105,6 @@
android:value="de.danoeh.antennapod.activity.MainActivity"/>
</activity>
<activity
android:name=".activity.FeedInfoActivity"
android:label="@string/feed_info_label">
</activity>
<activity
android:name=".activity.FeedSettingsActivity"
android:windowSoftInputMode="stateHidden"
android:label="@string/feed_settings_label">
</activity>
<service
android:name=".core.service.PlayerWidgetJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
@ -150,13 +139,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
</activity>
<activity
android:name=".activity.StatisticsActivity"
android:label="@string/statistics_label">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.activity.PreferenceActivity"/>
</activity>
<activity
android:name=".activity.ImportExportActivity"
android:label="@string/import_export">

View File

@ -63,6 +63,8 @@ public class PodcastApp extends Application {
EventBus.builder()
.addIndex(new ApEventBusIndex())
.addIndex(new ApCoreEventBusIndex())
.logNoSubscriberMessages(false)
.sendNoSubscriberEvent(false)
.installDefaultEventBus();
}

View File

@ -15,6 +15,7 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import de.danoeh.antennapod.core.util.IntentUtils;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
@ -57,8 +58,7 @@ public class AboutActivity extends AppCompatActivity {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("http")) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(browserIntent);
IntentUtils.openInBrowser(AboutActivity.this, url);
return true;
} else {
url = url.replace("file:///android_asset/", "");

View File

@ -1,19 +1,16 @@
package de.danoeh.antennapod.activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
import android.widget.Toast;
import de.danoeh.antennapod.CrashReportWriter;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.IntentUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
@ -45,13 +42,7 @@ public class BugReportActivity extends AppCompatActivity {
}
findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> {
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://github.com/AntennaPod/AntennaPod/issues"));
startActivity(myIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.pref_no_browser_found, Toast.LENGTH_LONG).show();
}
IntentUtils.openInBrowser(BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues");
});
findViewById(R.id.btn_copy_log).setOnClickListener(v -> {

View File

@ -1,225 +0,0 @@
package de.danoeh.antennapod.activity;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.joanzapata.iconify.Iconify;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LangUtils;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Displays information about a feed.
*/
public class FeedInfoActivity extends AppCompatActivity {
public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedInfoActivity";
private Feed feed;
private ImageView imgvCover;
private TextView txtvTitle;
private TextView txtvDescription;
private TextView lblLanguage;
private TextView txtvLanguage;
private TextView lblAuthor;
private TextView txtvAuthor;
private TextView txtvUrl;
private Disposable disposable;
private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() {
@Override
public void onClick(View v) {
if(feed != null && feed.getDownload_url() != null) {
String url = feed.getDownload_url();
ClipData clipData = ClipData.newPlainText(url, url);
android.content.ClipboardManager cm = (android.content.ClipboardManager) FeedInfoActivity.this
.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(clipData);
Toast t = Toast.makeText(FeedInfoActivity.this, R.string.copied_url_msg, Toast.LENGTH_SHORT);
t.show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
setContentView(R.layout.feedinfo);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1);
imgvCover = findViewById(R.id.imgvCover);
txtvTitle = findViewById(R.id.txtvTitle);
TextView txtvAuthorHeader = findViewById(R.id.txtvAuthor);
ImageView imgvBackground = findViewById(R.id.imgvBackground);
findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE);
findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE);
// https://github.com/bumptech/glide/issues/529
imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000));
txtvDescription = findViewById(R.id.txtvDescription);
lblLanguage = findViewById(R.id.lblLanguage);
txtvLanguage = findViewById(R.id.txtvLanguage);
lblAuthor = findViewById(R.id.lblAuthor);
txtvAuthor = findViewById(R.id.txtvDetailsAuthor);
txtvUrl = findViewById(R.id.txtvUrl);
txtvUrl.setOnClickListener(copyUrlToClipboard);
disposable = Maybe.create((MaybeOnSubscribe<Feed>) emitter -> {
Feed feed = DBReader.getFeed(feedId);
if (feed != null) {
emitter.onSuccess(feed);
} else {
emitter.onComplete();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result;
Log.d(TAG, "Language is " + feed.getLanguage());
Log.d(TAG, "Author is " + feed.getAuthor());
Log.d(TAG, "URL is " + feed.getDownload_url());
Glide.with(FeedInfoActivity.this)
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(imgvCover);
Glide.with(FeedInfoActivity.this)
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transform(new FastBlurTransformation())
.dontAnimate())
.into(imgvBackground);
txtvTitle.setText(feed.getTitle());
String description = feed.getDescription();
if(description != null) {
if(Feed.TYPE_ATOM1.equals(feed.getType())) {
HtmlToPlainText formatter = new HtmlToPlainText();
Document feedDescription = Jsoup.parse(feed.getDescription());
description = StringUtils.trim(formatter.getPlainText(feedDescription));
}
} else {
description = "";
}
txtvDescription.setText(description);
if (!TextUtils.isEmpty(feed.getAuthor())) {
txtvAuthor.setText(feed.getAuthor());
txtvAuthorHeader.setText(feed.getAuthor());
} else {
lblAuthor.setVisibility(View.GONE);
txtvAuthor.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(feed.getLanguage())) {
txtvLanguage.setText(LangUtils.getLanguageString(feed.getLanguage()));
} else {
lblLanguage.setVisibility(View.GONE);
txtvLanguage.setVisibility(View.GONE);
}
txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}");
Iconify.addIcons(txtvUrl);
supportInvalidateOptionsMenu();
}, error -> {
Log.d(TAG, Log.getStackTraceString(error));
finish();
}, () -> {
Log.e(TAG, "Activity was started with invalid arguments");
finish();
});
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.feedinfo, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null);
menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null &&
IntentUtils.isCallable(this, new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink()))));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
try {
return FeedMenuHandler.onOptionsItemClicked(this, item, feed);
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(this,
e.getMessage());
}
return super.onOptionsItemSelected(item);
}
}
}

View File

@ -1,130 +0,0 @@
package de.danoeh.antennapod.activity;
import android.arch.lifecycle.ViewModelProviders;
import android.graphics.LightingColorFilter;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.FeedSettingsFragment;
import de.danoeh.antennapod.viewmodel.FeedSettingsViewModel;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Displays information about a feed.
*/
public class FeedSettingsActivity extends AppCompatActivity {
public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedSettingsActivity";
private Feed feed;
private Disposable disposable;
private ImageView imgvCover;
private TextView txtvTitle;
private ImageView imgvBackground;
private TextView txtvAuthorHeader;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
super.onCreate(savedInstanceState);
setContentView(R.layout.feedsettings);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
imgvCover = findViewById(R.id.imgvCover);
txtvTitle = findViewById(R.id.txtvTitle);
txtvAuthorHeader = findViewById(R.id.txtvAuthor);
imgvBackground = findViewById(R.id.imgvBackground);
findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE);
findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE);
// https://github.com/bumptech/glide/issues/529
imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000));
long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1);
disposable = ViewModelProviders.of(this).get(FeedSettingsViewModel.class).getFeed(feedId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result;
showFragment();
showHeader();
}, error -> {
Log.d(TAG, Log.getStackTraceString(error));
finish();
}, () -> {
Log.e(TAG, "Activity was started with invalid arguments");
finish();
});
}
private void showFragment() {
FeedSettingsFragment fragment = new FeedSettingsFragment();
fragment.setArguments(getIntent().getExtras());
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.settings_fragment_container, fragment);
fragmentTransaction.commit();
}
private void showHeader() {
txtvTitle.setText(feed.getTitle());
if (!TextUtils.isEmpty(feed.getAuthor())) {
txtvAuthorHeader.setText(feed.getAuthor());
}
Glide.with(FeedSettingsActivity.this)
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(imgvCover);
Glide.with(FeedSettingsActivity.this)
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transform(new FastBlurTransformation())
.dontAnimate())
.into(imgvBackground);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View File

@ -47,7 +47,6 @@ import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.event.ProgressEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
@ -70,6 +69,7 @@ import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.fragment.TransitionEffect;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.danoeh.antennapod.preferences.PreferenceUpgrader;
import io.reactivex.Observable;
@ -378,15 +378,34 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
}
}
public void loadChildFragment(Fragment fragment) {
public void loadChildFragment(Fragment fragment, TransitionEffect transition) {
Validate.notNull(fragment);
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction()
.replace(R.id.main_view, fragment, "main")
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
switch (transition) {
case FADE:
transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out);
break;
case FLIP:
transaction.setCustomAnimations(
R.anim.card_flip_left_in,
R.anim.card_flip_left_out,
R.anim.card_flip_right_in,
R.anim.card_flip_right_out);
break;
}
transaction
.hide(getSupportFragmentManager().findFragmentByTag("main"))
.add(R.id.main_view, fragment, "main")
.addToBackStack(null)
.commit();
}
public void loadChildFragment(Fragment fragment) {
loadChildFragment(fragment, TransitionEffect.NONE);
}
public void dismissChildFragment() {
getSupportFragmentManager().popBackStack();
}
@ -787,25 +806,6 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
loadData();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(ProgressEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
switch(event.action) {
case START:
pd = new ProgressDialog(this);
pd.setMessage(event.message);
pd.setIndeterminate(true);
pd.setCancelable(false);
pd.show();
break;
case END:
if(pd != null) {
pd.dismiss();
}
break;
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(MessageEvent event) {
Log.d(TAG, "onEvent(" + event + ")");

View File

@ -34,6 +34,7 @@ import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.FontAwesomeIcons;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
@ -63,6 +64,9 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
@ -194,6 +198,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
};
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
onPositionObserverUpdate();
}
private static TextView getTxtvFFFromActivity(MediaplayerActivity activity) {
return activity.txtvFF;
}
@ -274,6 +283,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
controller.init();
loadMediaInfo();
onPositionObserverUpdate();
EventBus.getDefault().register(this);
}
@Override
@ -286,6 +296,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
if (disposable != null) {
disposable.dispose();
}
EventBus.getDefault().unregister(this);
super.onStop();
}
@ -452,8 +463,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
}
break;
case R.id.visit_website_item:
Uri uri = Uri.parse(getWebsiteLinkWithFallback(media));
startActivity(new Intent(Intent.ACTION_VIEW, uri));
IntentUtils.openInBrowser(MediaplayerActivity.this, getWebsiteLinkWithFallback(media));
break;
case R.id.share_link_item:
if (feedItem != null) {

View File

@ -122,7 +122,6 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
disposable.dispose();
}
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
saveCurrentFragment();
}
@ -175,7 +174,6 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem
protected void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadData();
}

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.adapter;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
@ -23,10 +24,12 @@ import android.widget.TextView;
import com.joanzapata.iconify.Iconify;
import java.lang.ref.WeakReference;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.storage.DownloadRequester;
@ -50,6 +53,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR
private final boolean showOnlyNewEpisodes;
private FeedItem selectedItem;
private Holder currentlyPlayingItem = null;
private final int playingBackGroundColor;
private final int normalBackGroundColor;
@ -165,8 +169,9 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR
holder.progress.setVisibility(View.INVISIBLE);
}
if(media.isCurrentlyPlaying()) {
if (media.isCurrentlyPlaying()) {
holder.container.setBackgroundColor(playingBackGroundColor);
currentlyPlayingItem = holder;
} else {
holder.container.setBackgroundColor(normalBackGroundColor);
}
@ -196,6 +201,22 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR
.load();
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int pos, List<Object> payload) {
onBindViewHolder(holder, pos);
if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) {
PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0);
holder.progress.setProgress((int) (100.0 * event.getPosition() / event.getDuration()));
}
}
public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event) {
if (currentlyPlayingItem != null && currentlyPlayingItem.getAdapterPosition() != RecyclerView.NO_POSITION) {
notifyItemChanged(currentlyPlayingItem.getAdapterPosition(), event);
}
}
@Nullable
public FeedItem getSelectedItem() {
return selectedItem;

View File

@ -13,15 +13,18 @@ import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.ThemeUtils;
@ -39,6 +42,8 @@ public class FeedItemlistAdapter extends BaseAdapter {
private final int playingBackGroundColor;
private final int normalBackGroundColor;
private int currentlyPlayingItem = -1;
public FeedItemlistAdapter(Context context,
ItemAccess itemAccess,
boolean showFeedtitle,
@ -176,8 +181,9 @@ public class FeedItemlistAdapter extends BaseAdapter {
}
typeDrawables.recycle();
if(media.isCurrentlyPlaying()) {
if (media.isCurrentlyPlaying()) {
holder.container.setBackgroundColor(playingBackGroundColor);
currentlyPlayingItem = position;
} else {
holder.container.setBackgroundColor(normalBackGroundColor);
}
@ -195,6 +201,19 @@ public class FeedItemlistAdapter extends BaseAdapter {
return convertView;
}
public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event, ListView listView) {
if (currentlyPlayingItem != -1 && currentlyPlayingItem < getCount()) {
View view = listView.getChildAt(currentlyPlayingItem
- listView.getFirstVisiblePosition() + listView.getHeaderViewsCount());
if (view == null) {
return;
}
Holder holder = (Holder) view.getTag();
holder.episodeProgress.setProgress((int) (100.0 * event.getPosition() / event.getDuration()));
holder.lenSize.setText(Converter.getDurationStringLong(event.getDuration() - event.getPosition()));
}
}
static class Holder {
LinearLayout container;
TextView title;

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.adapter;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.MotionEventCompat;
@ -24,9 +25,11 @@ import android.widget.TextView;
import com.joanzapata.iconify.Iconify;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
@ -57,6 +60,7 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap
private boolean locked;
private FeedItem selectedItem;
private ViewHolder currentlyPlayingItem = null;
private final int playingBackGroundColor;
private final int normalBackGroundColor;
@ -93,6 +97,18 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap
});
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int pos, List<Object> payload) {
onBindViewHolder(holder, pos);
if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) {
PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0);
holder.progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration()));
holder.progressLeft.setText(Converter.getDurationStringLong(event.getPosition()));
holder.progressRight.setText(Converter.getDurationStringLong(event.getDuration()));
}
}
@Nullable
public FeedItem getSelectedItem() {
return selectedItem;
@ -108,6 +124,12 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap
return itemAccess.getCount();
}
public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event) {
if (currentlyPlayingItem != null && currentlyPlayingItem.getAdapterPosition() != RecyclerView.NO_POSITION) {
notifyItemChanged(currentlyPlayingItem.getAdapterPosition(), event);
}
}
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener,
View.OnCreateContextMenuListener,
@ -287,6 +309,7 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap
if(media.isCurrentlyPlaying()) {
container.setBackgroundColor(playingBackGroundColor);
currentlyPlayingItem = this;
} else {
container.setBackgroundColor(normalBackGroundColor);
}

View File

@ -15,6 +15,7 @@ import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.util.IntentUtils;
public class RatingDialog {
@ -59,14 +60,10 @@ public class RatingDialog {
private static void rateNow() {
Context context = mContext.get();
if(context == null) {
if (context == null) {
return;
}
final String appPackage = "de.danoeh.antennapod";
final Uri uri = Uri.parse("https://play.google.com/store/apps/details?id=" + appPackage);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
IntentUtils.openInBrowser(context, "https://play.google.com/store/apps/details?id=de.danoeh.antennapod");
saveRated();
}

View File

@ -24,6 +24,7 @@ import android.widget.Toast;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@ -39,17 +40,16 @@ import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.view.EmptyViewHandler;
@ -197,10 +197,7 @@ public abstract class EpisodesListFragment extends Fragment {
if (!super.onOptionsItemSelected(item)) {
switch (item.getItemId()) {
case R.id.refresh_item:
List<Feed> feeds = ((MainActivity) getActivity()).getFeeds();
if (feeds != null) {
DBTasks.refreshAllFeeds(getActivity(), feeds);
}
AutoUpdateManager.runImmediate(requireContext());
return true;
case R.id.mark_all_read_item:
ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(),
@ -388,6 +385,13 @@ public abstract class EpisodesListFragment extends Fragment {
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (listAdapter != null) {
listAdapter.notifyCurrentlyPlayingItemChanged(event);
}
}
protected boolean shouldUpdatedItemRemainInList(FeedItem item) {
return true;
}
@ -397,7 +401,7 @@ public abstract class EpisodesListFragment extends Fragment {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
downloaderList = update.downloaders;
if (isMenuInvalidationAllowed && isUpdatingFeeds != update.feedIds.length > 0) {
if (isMenuInvalidationAllowed && event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) {
requireActivity().invalidateOptionsMenu();
}
if (update.mediaIds.length > 0) {

View File

@ -18,7 +18,7 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
@ -28,6 +28,9 @@ import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Fragment which is supposed to be displayed outside of the MediaplayerActivity
@ -138,6 +141,7 @@ public class ExternalPlayerFragment extends Fragment {
controller = setupPlaybackController();
controller.init();
loadMediaInfo();
EventBus.getDefault().register(this);
}
@Override
@ -147,6 +151,12 @@ public class ExternalPlayerFragment extends Fragment {
controller.release();
controller = null;
}
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
onPositionObserverUpdate();
}
@Override

View File

@ -0,0 +1,234 @@
package de.danoeh.antennapod.fragment;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.joanzapata.iconify.Iconify;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.LangUtils;
import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
/**
* Displays information about a feed.
*/
public class FeedInfoFragment extends Fragment {
private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedInfoActivity";
private Feed feed;
private Disposable disposable;
private ImageView imgvCover;
private TextView txtvTitle;
private TextView txtvDescription;
private TextView lblLanguage;
private TextView txtvLanguage;
private TextView lblAuthor;
private TextView txtvAuthor;
private TextView txtvUrl;
private TextView txtvAuthorHeader;
private ImageView imgvBackground;
public static FeedInfoFragment newInstance(Feed feed) {
FeedInfoFragment fragment = new FeedInfoFragment();
Bundle arguments = new Bundle();
arguments.putLong(EXTRA_FEED_ID, feed.getId());
fragment.setArguments(arguments);
return fragment;
}
private final View.OnClickListener copyUrlToClipboard = new View.OnClickListener() {
@Override
public void onClick(View v) {
if(feed != null && feed.getDownload_url() != null) {
String url = feed.getDownload_url();
ClipData clipData = ClipData.newPlainText(url, url);
android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext()
.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(clipData);
Toast t = Toast.makeText(getContext(), R.string.copied_url_msg, Toast.LENGTH_SHORT);
t.show();
}
}
};
@Override
public void onResume() {
super.onResume();
((MainActivity)getActivity()).getSupportActionBar().setTitle(R.string.feed_info_label);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.feedinfo, null);
setHasOptionsMenu(true);
imgvCover = root.findViewById(R.id.imgvCover);
txtvTitle = root.findViewById(R.id.txtvTitle);
txtvAuthorHeader = root.findViewById(R.id.txtvAuthor);
imgvBackground = root.findViewById(R.id.imgvBackground);
root.findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE);
root.findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE);
// https://github.com/bumptech/glide/issues/529
imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000));
txtvDescription = root.findViewById(R.id.txtvDescription);
lblLanguage = root.findViewById(R.id.lblLanguage);
txtvLanguage = root.findViewById(R.id.txtvLanguage);
lblAuthor = root.findViewById(R.id.lblAuthor);
txtvAuthor = root.findViewById(R.id.txtvDetailsAuthor);
txtvUrl = root.findViewById(R.id.txtvUrl);
txtvUrl.setOnClickListener(copyUrlToClipboard);
postponeEnterTransition();
return root;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
long feedId = getArguments().getLong(EXTRA_FEED_ID);
disposable = Maybe.create((MaybeOnSubscribe<Feed>) emitter -> {
Feed feed = DBReader.getFeed(feedId);
if (feed != null) {
emitter.onSuccess(feed);
} else {
emitter.onComplete();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result;
showFeed();
}, error -> Log.d(TAG, Log.getStackTraceString(error)),
this::startPostponedEnterTransition);
}
private void showFeed() {
Log.d(TAG, "Language is " + feed.getLanguage());
Log.d(TAG, "Author is " + feed.getAuthor());
Log.d(TAG, "URL is " + feed.getDownload_url());
Glide.with(getContext())
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(imgvCover);
Glide.with(getContext())
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.transform(new FastBlurTransformation())
.dontAnimate())
.into(imgvBackground);
txtvTitle.setText(feed.getTitle());
String description = feed.getDescription();
if(description != null) {
if(Feed.TYPE_ATOM1.equals(feed.getType())) {
HtmlToPlainText formatter = new HtmlToPlainText();
Document feedDescription = Jsoup.parse(feed.getDescription());
description = StringUtils.trim(formatter.getPlainText(feedDescription));
}
} else {
description = "";
}
txtvDescription.setText(description);
if (!TextUtils.isEmpty(feed.getAuthor())) {
txtvAuthor.setText(feed.getAuthor());
txtvAuthorHeader.setText(feed.getAuthor());
} else {
lblAuthor.setVisibility(View.GONE);
txtvAuthor.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(feed.getLanguage())) {
txtvLanguage.setText(LangUtils.getLanguageString(feed.getLanguage()));
} else {
lblLanguage.setVisibility(View.GONE);
txtvLanguage.setVisibility(View.GONE);
}
txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}");
Iconify.addIcons(txtvUrl);
getActivity().invalidateOptionsMenu();
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.feedinfo, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null);
menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null &&
IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink()))));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
boolean handled = false;
try {
handled = FeedMenuHandler.onOptionsItemClicked(getContext(), item, feed);
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(getContext(), e.getMessage());
}
return handled || super.onOptionsItemSelected(item);
}
}

View File

@ -29,6 +29,7 @@ import com.bumptech.glide.request.RequestOptions;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.widget.IconTextView;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@ -37,8 +38,6 @@ import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.FeedInfoActivity;
import de.danoeh.antennapod.activity.FeedSettingsActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
@ -140,37 +139,33 @@ public class FeedItemlistFragment extends ListFragment {
}
@Override
public void onStart() {
super.onStart();
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && getActivity() != null) {
((MainActivity) getActivity()).getSupportActionBar().setTitle("");
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
registerForContextMenu(getListView());
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadItems();
}
@Override
public void onResume() {
super.onResume();
((MainActivity)getActivity()).getSupportActionBar().setTitle("");
updateProgressBarVisibility();
}
@Override
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
if(disposable != null) {
disposable.dispose();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
resetViewState();
}
private void resetViewState() {
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
if (disposable != null) {
disposable.dispose();
}
adapter = null;
listFooter = null;
}
@ -343,13 +338,6 @@ public class FeedItemlistFragment extends ListFragment {
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
registerForContextMenu(getListView());
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
if(adapter == null) {
@ -390,14 +378,21 @@ public class FeedItemlistFragment extends ListFragment {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
downloaderList = update.downloaders;
if (isUpdatingFeed != event.update.feedIds.length > 0) {
if (event.hasChangedFeedUpdateStatus(isUpdatingFeed)) {
updateProgressBarVisibility();
}
if(adapter != null && update.mediaIds.length > 0) {
if (adapter != null && update.mediaIds.length > 0) {
adapter.notifyDataSetChanged();
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
adapter.notifyCurrentlyPlayingItemChanged(event, getListView());
}
}
private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
@ -421,8 +416,9 @@ public class FeedItemlistFragment extends ListFragment {
}
private void onFragmentLoaded() {
if(!isVisible()) {
private void displayList() {
if (getView() == null) {
Log.e(TAG, "Required root view is not yet created. Stop binding data to UI.");
return;
}
if (adapter == null) {
@ -506,10 +502,8 @@ public class FeedItemlistFragment extends ListFragment {
imgvCover.setOnClickListener(v -> showFeedInfo());
butShowSettings.setOnClickListener(v -> {
if (feed != null) {
Intent startIntent = new Intent(getActivity(), FeedSettingsActivity.class);
startIntent.putExtra(FeedSettingsActivity.EXTRA_FEED_ID,
feed.getId());
startActivity(startIntent);
FeedSettingsFragment fragment = FeedSettingsFragment.newInstance(feed);
((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.FLIP);
}
});
headerCreated = true;
@ -517,10 +511,8 @@ public class FeedItemlistFragment extends ListFragment {
private void showFeedInfo() {
if (feed != null) {
Intent startIntent = new Intent(getActivity(), FeedInfoActivity.class);
startIntent.putExtra(FeedInfoActivity.EXTRA_FEED_ID,
feed.getId());
startActivity(startIntent);
FeedInfoFragment fragment = FeedInfoFragment.newInstance(feed);
((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.FLIP);
}
}
@ -626,7 +618,7 @@ public class FeedItemlistFragment extends ListFragment {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result.orElse(null);
onFragmentLoaded();
displayList();
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}

View File

@ -1,39 +1,66 @@
package de.danoeh.antennapod.fragment;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.util.Log;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFilter;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.dialog.EpisodeFilterDialog;
import de.danoeh.antennapod.viewmodel.FeedSettingsViewModel;
import static de.danoeh.antennapod.activity.FeedSettingsActivity.EXTRA_FEED_ID;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class FeedSettingsFragment extends PreferenceFragmentCompat {
private static final CharSequence PREF_EPISODE_FILTER = "episodeFilter";
private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
private static final String TAG = "FeedSettingsFragment";
private Feed feed;
private Disposable disposable;
private FeedPreferences feedPreferences;
public static FeedSettingsFragment newInstance(Feed feed) {
FeedSettingsFragment fragment = new FeedSettingsFragment();
Bundle arguments = new Bundle();
arguments.putLong(EXTRA_FEED_ID, feed.getId());
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.feed_settings);
postponeEnterTransition();
long feedId = getArguments().getLong(EXTRA_FEED_ID);
ViewModelProviders.of(getActivity()).get(FeedSettingsViewModel.class).getFeed(feedId)
disposable = Maybe.create((MaybeOnSubscribe<Feed>) emitter -> {
Feed feed = DBReader.getFeed(feedId);
if (feed != null) {
emitter.onSuccess(feed);
} else {
emitter.onComplete();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
feed = result;
feedPreferences = feed.getPreferences();
((MainActivity) getActivity()).getSupportActionBar().setSubtitle(feed.getTitle());
setupAutoDownloadPreference();
setupKeepUpdatedPreference();
@ -43,7 +70,31 @@ public class FeedSettingsFragment extends PreferenceFragmentCompat {
updateAutoDeleteSummary();
updateAutoDownloadEnabled();
}).dispose();
}, error -> Log.d(TAG, Log.getStackTraceString(error)),
this::startPostponedEnterTransition);
}
@Override
public void onResume() {
super.onResume();
((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.feed_settings_label);
if (feed != null) {
((MainActivity) getActivity()).getSupportActionBar().setSubtitle(feed.getTitle());
}
}
@Override
public void onStop() {
super.onStop();
((MainActivity) getActivity()).getSupportActionBar().setSubtitle(null);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
}
private void setupEpisodeFilterPreference() {

View File

@ -96,13 +96,7 @@ public class ItemDescriptionFragment extends Fragment {
if (Timeline.isTimecodeLink(url)) {
onTimecodeLinkSelected(url);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
return true;
}
IntentUtils.openInBrowser(getContext(), url);
}
return true;
}
@ -159,11 +153,7 @@ public class ItemDescriptionFragment extends Fragment {
if (selectedURL != null) {
switch (item.getItemId()) {
case R.id.open_in_browser_item:
Uri uri = Uri.parse(selectedURL);
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if(IntentUtils.isCallable(getActivity(), intent)) {
getActivity().startActivity(intent);
}
IntentUtils.openInBrowser(getContext(), selectedURL);
break;
case R.id.share_url_item:
ShareUtils.shareLink(getActivity(), selectedURL);

View File

@ -210,10 +210,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
webvDescription.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if(IntentUtils.isCallable(getActivity(), intent)) {
startActivity(intent);
}
IntentUtils.openInBrowser(getContext(), url);
return true;
}
});
@ -484,11 +481,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
if (selectedURL != null) {
switch (item.getItemId()) {
case R.id.open_in_browser_item:
Uri uri = Uri.parse(selectedURL);
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if(IntentUtils.isCallable(getActivity(), intent)) {
getActivity().startActivity(intent);
}
IntentUtils.openInBrowser(getContext(), selectedURL);
break;
case R.id.share_url_item:
ShareUtils.shareLink(getActivity(), selectedURL);

View File

@ -26,6 +26,7 @@ import android.widget.TextView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@ -41,14 +42,12 @@ import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
@ -56,6 +55,7 @@ import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.QueueSorter;
import de.danoeh.antennapod.core.util.SortOrder;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
@ -201,8 +201,8 @@ public class QueueFragment extends Fragment {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
downloaderList = update.downloaders;
if (isUpdatingFeeds != update.feedIds.length > 0) {
getActivity().supportInvalidateOptionsMenu();
if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) {
getActivity().invalidateOptionsMenu();
}
if (recyclerAdapter != null && update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
@ -214,6 +214,13 @@ public class QueueFragment extends Fragment {
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (recyclerAdapter != null) {
recyclerAdapter.notifyCurrentlyPlayingItemChanged(event);
}
}
private void saveScrollPosition() {
int firstItem = layoutManager.findFirstVisibleItemPosition();
View firstItemView = layoutManager.findViewByPosition(firstItem);
@ -305,10 +312,7 @@ public class QueueFragment extends Fragment {
toggleQueueLock();
return true;
case R.id.refresh_item:
List<Feed> feeds = ((MainActivity) getActivity()).getFeeds();
if (feeds != null) {
DBTasks.refreshAllFeeds(getActivity(), feeds);
}
AutoUpdateManager.runImmediate(requireContext());
return true;
case R.id.clear_queue:
// make sure the user really wants to clear the queue

View File

@ -25,19 +25,28 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.SubscriptionsAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Fragment for displaying feed subscriptions
@ -56,6 +65,7 @@ public class SubscriptionFragment extends Fragment {
private SubscriptionsAdapter subscriptionAdapter;
private int mPosition = -1;
private boolean isUpdatingFeeds = false;
private Disposable disposable;
private SharedPreferences prefs;
@ -89,6 +99,8 @@ public class SubscriptionFragment extends Fragment {
menu.findItem(R.id.subscription_num_columns_3).setChecked(columns == 3);
menu.findItem(R.id.subscription_num_columns_4).setChecked(columns == 4);
menu.findItem(R.id.subscription_num_columns_5).setChecked(columns == 5);
isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker);
}
@Override
@ -97,6 +109,9 @@ public class SubscriptionFragment extends Fragment {
return true;
}
switch (item.getItemId()) {
case R.id.refresh_item:
AutoUpdateManager.runImmediate(requireContext());
return true;
case R.id.subscription_num_columns_2:
setColumnNumber(2);
return true;
@ -136,6 +151,7 @@ public class SubscriptionFragment extends Fragment {
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadSubscriptions();
}
@ -143,6 +159,7 @@ public class SubscriptionFragment extends Fragment {
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
if(disposable != null) {
disposable.dispose();
}
@ -278,6 +295,17 @@ public class SubscriptionFragment extends Fragment {
}
};
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) {
getActivity().invalidateOptionsMenu();
}
}
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker =
() -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds();
private final SubscriptionsAdapter.ItemAccess itemAccess = new SubscriptionsAdapter.ItemAccess() {
@Override
public int getCount() {

View File

@ -0,0 +1,5 @@
package de.danoeh.antennapod.fragment;
public enum TransitionEffect {
NONE, FLIP, FADE
}

View File

@ -1,29 +1,42 @@
package de.danoeh.antennapod.fragment.preferences;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
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.PreferenceScreen;
import android.util.Log;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat {
private static final String TAG = "AutoDnldPrefFragment";
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1;
private static final String PREF_KEY_LOCATION_PERMISSION_REQUEST_PROMPT = "prefAutoDownloadWifiFilterAndroid10PermissionPrompt";
private CheckBoxPreference[] selectedNetworks;
private Preference prefPermissionRequestPromptOnAndroid10 = null;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_autodownload);
@ -34,6 +47,12 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat {
buildEpisodeCleanupPreference();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.auto_download_label);
}
@Override
public void onResume() {
super.onResume();
@ -175,10 +194,65 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat {
}
private void setSelectedNetworksEnabled(boolean b) {
if (showPermissionRequestPromptOnAndroid10IfNeeded(b)) {
return;
}
if (selectedNetworks != null) {
for (Preference p : selectedNetworks) {
p.setEnabled(b);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode != LOCATION_PERMISSION_REQUEST_CODE) {
return;
}
if (permissions.length > 0 && permissions[0].equals(Manifest.permission.ACCESS_FINE_LOCATION) &&
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
buildAutodownloadSelectedNetworksPreference();
}
}
private boolean showPermissionRequestPromptOnAndroid10IfNeeded(boolean wifiFilterEnabled) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
return false;
}
// Cases Android 10(Q) or later
if (prefPermissionRequestPromptOnAndroid10 != null) {
getPreferenceScreen().removePreference(prefPermissionRequestPromptOnAndroid10);
prefPermissionRequestPromptOnAndroid10 = null;
}
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
return false;
}
// Case location permission not yet granted, permission-specific UI is needed
if (!wifiFilterEnabled) {
// Don't show the UI when WiFi filter disabled.
// it still return true, so that the caller knows
// it does not have required permission, and will not invoke codes that require so.
return true;
}
Preference pref = new Preference(requireActivity());
pref.setKey(PREF_KEY_LOCATION_PERMISSION_REQUEST_PROMPT);
pref.setTitle(R.string.autodl_wifi_filter_permission_title);
pref.setSummary(R.string.autodl_wifi_filter_permission_message);
pref.setIcon(R.drawable.ic_warning_red);
pref.setOnPreferenceClickListener(preference -> {
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST_CODE);
return true;
});
pref.setPersistent(false);
getPreferenceScreen().addPreference(pref);
prefPermissionRequestPromptOnAndroid10 = pref;
return true;
}
}

View File

@ -9,6 +9,7 @@ import android.text.Html;
import android.text.format.DateUtils;
import android.widget.Toast;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
@ -29,6 +30,12 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
setupGpodderScreen();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.gpodnet_main_label);
}
@Override
public void onResume() {
super.onResume();

View File

@ -14,6 +14,12 @@ public class IntegrationsPreferencesFragment extends PreferenceFragmentCompat {
setupIntegrationsScreen();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.integrations_label);
}
private void setupIntegrationsScreen() {
findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_gpodder);

View File

@ -1,20 +1,16 @@
package de.danoeh.antennapod.fragment.preferences;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.util.Log;
import android.widget.Toast;
import com.bytehamster.lib.preferencesearch.SearchConfiguration;
import com.bytehamster.lib.preferencesearch.SearchPreference;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AboutActivity;
import de.danoeh.antennapod.activity.BugReportActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.activity.StatisticsActivity;
import de.danoeh.antennapod.core.util.IntentUtils;
public class MainPreferencesFragment extends PreferenceFragmentCompat {
private static final String TAG = "MainPreferencesFragment";
@ -37,6 +33,12 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
setupSearch();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_label);
}
private void setupMainScreen() {
findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_user_interface);
@ -67,16 +69,17 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
);
findPreference(STATISTICS).setOnPreferenceClickListener(
preference -> {
startActivity(new Intent(getActivity(), StatisticsActivity.class));
getFragmentManager().beginTransaction().replace(R.id.content, new StatisticsFragment())
.addToBackStack(getString(R.string.statistics_label)).commit();
return true;
}
);
findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> {
openInBrowser("https://antennapod.org/faq.html");
IntentUtils.openInBrowser(getContext(), "https://antennapod.org/faq.html");
return true;
});
findPreference(PREF_VIEW_MAILING_LIST).setOnPreferenceClickListener(preference -> {
openInBrowser("https://groups.google.com/forum/#!forum/antennapod");
IntentUtils.openInBrowser(getContext(), "https://groups.google.com/forum/#!forum/antennapod");
return true;
});
findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> {
@ -85,16 +88,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
});
}
private void openInBrowser(String url) {
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(myIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.pref_no_browser_found, Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}
private void setupSearch() {
SearchPreference searchPreference = (SearchPreference) findPreference("searchPreference");
SearchConfiguration config = searchPreference.getSearchConfiguration();

View File

@ -30,6 +30,12 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat {
setupNetworkScreen();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.network_pref);
}
@Override
public void onResume() {
super.onResume();

View File

@ -8,6 +8,7 @@ import android.support.v7.preference.ListPreference;
import android.support.v7.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MediaplayerActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
@ -27,6 +28,12 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
buildSmartMarkAsPlayedPreference();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.playback_pref);
}
@Override
public void onResume() {
super.onResume();

View File

@ -1,23 +1,27 @@
package de.danoeh.antennapod.activity;
package de.danoeh.antennapod.fragment.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.adapter.StatisticsListAdapter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.Converter;
import io.reactivex.Observable;
@ -28,10 +32,8 @@ import io.reactivex.schedulers.Schedulers;
/**
* Displays the 'statistics' screen
*/
public class StatisticsActivity extends AppCompatActivity
implements AdapterView.OnItemClickListener {
private static final String TAG = StatisticsActivity.class.getSimpleName();
public class StatisticsFragment extends Fragment implements AdapterView.OnItemClickListener {
private static final String TAG = StatisticsFragment.class.getSimpleName();
private static final String PREF_NAME = "StatisticsActivityPrefs";
private static final String PREF_COUNT_ALL = "countAll";
@ -44,54 +46,51 @@ public class StatisticsActivity extends AppCompatActivity
private SharedPreferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(UserPreferences.getTheme());
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayShowHomeEnabled(true);
setContentView(R.layout.statistics_activity);
prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
countAll = prefs.getBoolean(PREF_COUNT_ALL, false);
setHasOptionsMenu(true);
}
totalTimeTextView = findViewById(R.id.total_time);
feedStatisticsList = findViewById(R.id.statistics_list);
progressBar = findViewById(R.id.progressBar);
listAdapter = new StatisticsListAdapter(this);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.statistics_activity, container, false);
totalTimeTextView = root.findViewById(R.id.total_time);
feedStatisticsList = root.findViewById(R.id.statistics_list);
progressBar = root.findViewById(R.id.progressBar);
listAdapter = new StatisticsListAdapter(getContext());
listAdapter.setCountAll(countAll);
feedStatisticsList.setAdapter(listAdapter);
feedStatisticsList.setOnItemClickListener(this);
return root;
}
@Override
public void onResume() {
super.onResume();
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.statistics_label);
refreshStatistics();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.statistics, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.statistics_mode) {
if (item.getItemId() == R.id.statistics_mode) {
selectStatisticsMode();
return true;
} else {
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
private void selectStatisticsMode() {
View contentView = View.inflate(this, R.layout.statistics_mode_select_dialog, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View contentView = View.inflate(getContext(), R.layout.statistics_mode_select_dialog, null);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setView(contentView);
builder.setTitle(R.string.statistics_mode);
@ -126,8 +125,8 @@ public class StatisticsActivity extends AppCompatActivity
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
totalTimeTextView.setText(Converter
.shortLocalizedDuration(this, countAll ? result.totalTimeCountAll : result.totalTime));
totalTimeTextView.setText(Converter.shortLocalizedDuration(getContext(),
countAll ? result.totalTimeCountAll : result.totalTime));
listAdapter.update(result.feedTime);
progressBar.setVisibility(View.GONE);
totalTimeTextView.setVisibility(View.VISIBLE);
@ -139,14 +138,13 @@ public class StatisticsActivity extends AppCompatActivity
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DBReader.StatisticsItem stats = listAdapter.getItem(position);
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setTitle(stats.feed.getTitle());
dialog.setMessage(getString(R.string.statistics_details_dialog,
countAll ? stats.episodesStartedIncludingMarked : stats.episodesStarted,
stats.episodes,
Converter.shortLocalizedDuration(this, countAll ?
stats.timePlayedCountAll : stats.timePlayed),
Converter.shortLocalizedDuration(this, stats.time)));
stats.episodes, Converter.shortLocalizedDuration(getContext(),
countAll ? stats.timePlayedCountAll : stats.timePlayed),
Converter.shortLocalizedDuration(getContext(), stats.time)));
dialog.setPositiveButton(android.R.string.ok, null);
dialog.show();
}

View File

@ -23,6 +23,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.DirectoryChooserActivity;
import de.danoeh.antennapod.activity.ImportExportActivity;
import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.asynctask.DocumentFileExportWorker;
import de.danoeh.antennapod.asynctask.ExportWorker;
import de.danoeh.antennapod.core.export.ExportWriter;
@ -63,6 +64,12 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat {
setupStorageScreen();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.storage_pref);
}
@Override
public void onResume() {
super.onResume();

View File

@ -11,6 +11,7 @@ import android.widget.ListView;
import android.widget.Toast;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import org.apache.commons.lang3.ArrayUtils;
@ -25,6 +26,12 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat {
setupInterfaceScreen();
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.user_interface_label);
}
private void setupInterfaceScreen() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {

View File

@ -214,14 +214,7 @@ public class FeedItemMenuHandler {
DBWriter.setFeedItemAutoDownload(selectedItem, false);
break;
case R.id.visit_website_item:
Uri uri = Uri.parse(FeedItemUtil.getLinkWithFallback(selectedItem));
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if(IntentUtils.isCallable(context, intent)) {
context.startActivity(intent);
} else {
Toast.makeText(context, context.getString(R.string.download_error_malformed_url),
Toast.LENGTH_SHORT).show();
}
IntentUtils.openInBrowser(context, FeedItemUtil.getLinkWithFallback(selectedItem));
break;
case R.id.share_link_item:
ShareUtils.shareFeedItemLink(context, selectedItem);

View File

@ -86,14 +86,7 @@ public class FeedMenuHandler {
conDialog.createNewDialog().show();
break;
case R.id.visit_website_item:
Uri uri = Uri.parse(selectedFeed.getLink());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if(IntentUtils.isCallable(context, intent)) {
context.startActivity(intent);
} else {
Toast.makeText(context, context.getString(R.string.download_error_malformed_url),
Toast.LENGTH_SHORT).show();
}
IntentUtils.openInBrowser(context, selectedFeed.getLink());
break;
case R.id.share_link_item:
ShareUtils.shareFeedlink(context, selectedFeed);

View File

@ -3,9 +3,11 @@ package de.danoeh.antennapod.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
public class PreferenceUpgrader {
@ -22,7 +24,7 @@ public class PreferenceUpgrader {
if (oldVersion != newVersion) {
NotificationUtils.createChannels(context);
UserPreferences.restartUpdateAlarm();
AutoUpdateManager.restartUpdateAlarm();
upgrade(oldVersion);
upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply();

View File

@ -1,30 +0,0 @@
package de.danoeh.antennapod.viewmodel;
import android.arch.lifecycle.ViewModel;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.storage.DBReader;
import io.reactivex.Maybe;
public class FeedSettingsViewModel extends ViewModel {
private Feed feed;
public Maybe<Feed> getFeed(long feedId) {
if (feed == null) {
return loadFeed(feedId);
} else {
return Maybe.just(feed);
}
}
private Maybe<Feed> loadFeed(long feedId) {
return Maybe.create(emitter -> {
Feed feed = DBReader.getFeed(feedId);
if (feed != null) {
this.feed = feed;
emitter.onSuccess(feed);
} else {
emitter.onComplete();
}
});
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>

View File

@ -1,41 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/total_time_listened_to_podcasts"
android:gravity="center_horizontal"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_gravity="center_horizontal"
style="?android:attr/progressBarStyleLarge"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="@string/total_time_listened_to_podcasts"
android:gravity="center_horizontal"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/total_time"
android:gravity="center_horizontal"
android:textSize="45sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/total_time"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_horizontal"
android:textSize="28sp"
tools:text="10.0 hours"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_gravity="center_horizontal"
style="?android:attr/progressBarStyleSmall"/>
</LinearLayout>
<View
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/dividerVertical"/>
<ListView
android:id="@+id/statistics_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice"
android:clipToPadding="false"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:paddingBottom="@dimen/list_vertical_padding"
android:paddingTop="@dimen/list_vertical_padding"
android:scrollbarStyle="outsideOverlay" />
android:id="@+id/statistics_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice"
android:clipToPadding="false"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:paddingBottom="@dimen/list_vertical_padding"
android:paddingTop="@dimen/list_vertical_padding"
android:scrollbarStyle="outsideOverlay"
tools:listitem="@layout/statistics_listitem"/>
</LinearLayout>

View File

@ -3,63 +3,54 @@
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/listitem_iconwithtext_height"
android:paddingLeft="@dimen/listitem_threeline_verticalpadding"
android:paddingStart="@dimen/listitem_threeline_verticalpadding"
android:paddingRight="@dimen/listitem_threeline_verticalpadding"
android:paddingEnd="@dimen/listitem_threeline_verticalpadding"
tools:background="@android:color/darker_gray">
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/imgvCover"
android:contentDescription="@string/cover_label"
android:layout_width="@dimen/thumbnail_length_navlist"
android:layout_height="@dimen/thumbnail_length_navlist"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:adjustViewBounds="true"
android:cropToPadding="true"
android:scaleType="centerCrop"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
tools:src="@drawable/ic_antenna"
tools:background="@android:color/holo_green_dark"/>
android:id="@+id/imgvCover"
android:contentDescription="@string/cover_label"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:adjustViewBounds="true"
android:cropToPadding="true"
android:scaleType="centerCrop"
tools:src="@drawable/ic_antenna"
tools:background="@android:color/holo_green_dark"/>
<TextView
android:id="@+id/txtvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/list_vertical_padding"
android:layout_marginStart="@dimen/list_vertical_padding"
android:lines="1"
android:textColor="?android:attr/textColorTertiary"
android:textSize="@dimen/text_size_navdrawer"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
tools:text="23"
tools:background="@android:color/holo_green_dark"/>
android:id="@+id/txtvTitle"
android:lines="1"
android:ellipsize="end"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_toRightOf="@id/imgvCover"
android:layout_alignTop="@id/imgvCover"
android:layout_alignWithParentIfMissing="true"
tools:text="Feed title"/>
<TextView
android:id="@+id/txtvTitle"
android:lines="1"
android:ellipsize="end"
android:singleLine="true"
android:layout_centerVertical="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_navdrawer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/listitem_iconwithtext_textleftpadding"
android:layout_marginStart="@dimen/listitem_iconwithtext_textleftpadding"
android:layout_toRightOf="@id/imgvCover"
android:layout_toEndOf="@id/imgvCover"
android:layout_toLeftOf="@id/txtvTime"
android:layout_toStartOf="@id/txtvTime"
android:layout_alignWithParentIfMissing="true"
tools:text="Navigation feed item title"
tools:background="@android:color/holo_green_dark"/>
android:id="@+id/txtvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="?android:attr/textColorTertiary"
android:textSize="14sp"
android:layout_toEndOf="@+id/imgvCover"
android:layout_toRightOf="@+id/imgvCover"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_below="@+id/txtvTitle"
tools:text="23 hours"/>
</RelativeLayout>

View File

@ -2,6 +2,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/refresh_item"
android:title="@string/refresh_label"
android:menuCategory="container"
custom:showAsAction="always"
android:icon="?attr/navigation_refresh"/>
<item
android:id="@+id/subscription_num_columns"
android:title="@string/subscription_num_columns"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="card_flip_time_full">400</integer>
<integer name="card_flip_time_half">200</integer>
</resources>

View File

@ -29,17 +29,34 @@ public abstract class CastEnabledActivity extends AppCompatActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String TAG = "CastEnabledActivity";
protected CastManager castManager;
protected SwitchableMediaRouteActionProvider mediaRouteActionProvider;
private CastConsumer castConsumer;
private CastManager castManager;
private SwitchableMediaRouteActionProvider mediaRouteActionProvider;
private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!CastManager.isInitialized()) {
return;
}
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).
registerOnSharedPreferenceChangeListener(this);
castConsumer = new DefaultCastConsumer() {
@Override
public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
onCastConnectionChanged(true);
}
@Override
public void onDisconnected() {
onCastConnectionChanged(false);
}
};
castManager = CastManager.getInstance();
castManager.addCastConsumer(castConsumer);
castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
@ -48,6 +65,10 @@ public abstract class CastEnabledActivity extends AppCompatActivity
@Override
protected void onDestroy() {
if (!CastManager.isInitialized()) {
super.onDestroy();
return;
}
PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
.unregisterOnSharedPreferenceChangeListener(this);
castManager.removeCastConsumer(castConsumer);
@ -58,6 +79,9 @@ public abstract class CastEnabledActivity extends AppCompatActivity
@CallSuper
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
if (!CastManager.isInitialized()) {
return true;
}
getMenuInflater().inflate(R.menu.cast_enabled, menu);
castButtonVisibilityManager.setMenu(menu);
return true;
@ -67,6 +91,10 @@ public abstract class CastEnabledActivity extends AppCompatActivity
@CallSuper
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (!CastManager.isInitialized()) {
return true;
}
MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
if (mediaRouteButton == null) {
Log.wtf(TAG, "MediaRoute item could not be found on the menu!", new Exception());
@ -83,15 +111,22 @@ public abstract class CastEnabledActivity extends AppCompatActivity
@Override
protected void onResume() {
super.onResume();
if (!CastManager.isInitialized()) {
return;
}
castButtonVisibilityManager.setResumed(true);
}
@Override
protected void onPause() {
super.onPause();
if (!CastManager.isInitialized()) {
return;
}
castButtonVisibilityManager.setResumed(false);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
@ -105,18 +140,6 @@ public abstract class CastEnabledActivity extends AppCompatActivity
}
}
CastConsumer castConsumer = new DefaultCastConsumer() {
@Override
public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
onCastConnectionChanged(true);
}
@Override
public void onDisconnected() {
onCastConnectionChanged(false);
}
};
private void onCastConnectionChanged(boolean connected) {
if (connected) {
castButtonVisibilityManager.onConnected();
@ -133,6 +156,9 @@ public abstract class CastEnabledActivity extends AppCompatActivity
* @param showAsAction refer to {@link MenuItem#setShowAsAction(int)}
*/
public final void requestCastButton(int showAsAction) {
if (!CastManager.isInitialized()) {
return;
}
castButtonVisibilityManager.requestCastButton(showAsAction);
}

View File

@ -1,8 +1,13 @@
package de.danoeh.antennapod.preferences;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
@ -18,6 +23,7 @@ public class PreferenceControllerFlavorHelper {
final int googlePlayServicesCheck = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(ui.getActivity());
if (googlePlayServicesCheck == ConnectionResult.SUCCESS) {
displayRestartRequiredDialog(ui.requireContext());
return true;
} else {
GoogleApiAvailability.getInstance()
@ -29,4 +35,13 @@ public class PreferenceControllerFlavorHelper {
return true;
});
}
private static void displayRestartRequiredDialog(@NonNull Context context) {
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(android.R.string.dialog_alert_title);
dialog.setMessage(R.string.pref_restart_required);
dialog.setPositiveButton(android.R.string.ok, null);
dialog.setCancelable(false);
dialog.show();
}
}

View File

@ -45,7 +45,6 @@ project.ext {
targetSdkVersion = 28
supportVersion = "27.1.1"
lifecycle_version = "1.1.1"
workManagerVersion = "1.0.1"
awaitilityVersion = "3.1.2"
commonsioVersion = "2.5"

View File

@ -10,6 +10,7 @@
<property name="fileExtensions" value="java, xml"/>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
@ -17,12 +18,65 @@
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="OneStatementPerLine"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
</module>
</module>

View File

@ -76,7 +76,6 @@ dependencies {
annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "org.awaitility:awaitility:$awaitilityVersion"
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerVersion"
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
@ -92,6 +91,7 @@ dependencies {
System.out.println("core: free build hack, skipping some dependencies")
}
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -25,4 +25,8 @@ public class DownloadEvent {
"update=" + update +
'}';
}
public boolean hasChangedFeedUpdateStatus(boolean oldStatus) {
return oldStatus != update.feedIds.length > 0;
}
}

View File

@ -0,0 +1,19 @@
package de.danoeh.antennapod.core.event;
public class PlaybackPositionEvent {
private final int position;
private final int duration;
public PlaybackPositionEvent(int position, int duration) {
this.position = position;
this.duration = duration;
}
public int getPosition() {
return position;
}
public int getDuration() {
return duration;
}
}

View File

@ -1,36 +0,0 @@
package de.danoeh.antennapod.core.event;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class ProgressEvent {
public enum Action {
START, END
}
public final Action action;
public final String message;
private ProgressEvent(Action action, String message) {
this.action = action;
this.message = message;
}
public static ProgressEvent start(String message) {
return new ProgressEvent(Action.START, message);
}
public static ProgressEvent end() {
return new ProgressEvent(Action.END, null);
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("action", action)
.append("message", message)
.toString();
}
}

View File

@ -218,6 +218,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
return itemIdentifier;
} else if (title != null && !title.isEmpty()) {
return title;
} else if (hasMedia() && media.getDownload_url() != null) {
return media.getDownload_url();
} else {
return link;
}

View File

@ -92,11 +92,9 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@Nullable
@Override
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
Log.d(TAG, "buildLoadData() called with: " + "model = [" + model + "], width = ["
+ width + "], height = [" + height + "]");
if(TextUtils.isEmpty(model)) {
if (TextUtils.isEmpty(model)) {
return null;
} else if(model.startsWith("/")) {
} else if (model.startsWith("/")) {
return new LoadData<>(new ObjectKey(model), new AudioCoverFetcher(model));
} else {
GlideUrl url = new GlideUrl(model);

View File

@ -4,7 +4,12 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.playback.Playable;
/**
* Provides access to preferences set by the playback service. A private
@ -19,35 +24,35 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
* Contains the feed id of the currently playing item if it is a FeedMedia
* object.
*/
public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
private static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
/**
* Contains the id of the currently playing FeedMedia object or
* NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object.
*/
public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
private static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId";
/**
* Type of the media object that is currently being played. This preference
* is set to NO_MEDIA_PLAYING after playback has been completed and is set
* as soon as the 'play' button is pressed.
*/
public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
private static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia";
/**
* True if last played media was streamed.
*/
public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
private static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
/**
* True if last played media was a video.
*/
public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
private static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo";
/**
* The current player status as int.
*/
public static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus";
private static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus";
/**
* Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing.
@ -87,10 +92,6 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
}
}
public static long getLastPlayedFeedId() {
return prefs.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1);
}
public static long getCurrentlyPlayingMedia() {
return prefs.getLong(PREF_CURRENTLY_PLAYING_MEDIA, NO_MEDIA_PLAYING);
}
@ -119,4 +120,52 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
editor.putInt(PREF_CURRENT_PLAYER_STATUS, PLAYER_STATUS_OTHER);
editor.apply();
}
public static void writeMediaPlaying(Playable playable, PlayerStatus playerStatus, boolean stream) {
Log.d(TAG, "Writing playback preferences");
SharedPreferences.Editor editor = prefs.edit();
if (playable == null) {
writeNoMediaPlaying();
} else {
editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA, playable.getPlayableType());
editor.putBoolean(PREF_CURRENT_EPISODE_IS_STREAM, stream);
editor.putBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, playable.getMediaType() == MediaType.VIDEO);
if (playable instanceof FeedMedia) {
FeedMedia fMedia = (FeedMedia) playable;
editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, fMedia.getItem().getFeed().getId());
editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, fMedia.getId());
} else {
editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, NO_MEDIA_PLAYING);
editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING);
}
playable.writeToPreferences(editor);
}
editor.putInt(PREF_CURRENT_PLAYER_STATUS, getCurrentPlayerStatusAsInt(playerStatus));
editor.apply();
}
public static void writePlayerStatus(PlayerStatus playerStatus) {
Log.d(TAG, "Writing player status playback preferences");
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_CURRENT_PLAYER_STATUS, getCurrentPlayerStatusAsInt(playerStatus));
editor.apply();
}
private static int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) {
int playerStatusAsInt;
switch (playerStatus) {
case PLAYING:
playerStatusAsInt = PLAYER_STATUS_PLAYING;
break;
case PAUSED:
playerStatusAsInt = PLAYER_STATUS_PAUSED;
break;
default:
playerStatusAsInt = PLAYER_STATUS_OTHER;
}
return playerStatusAsInt;
}
}

View File

@ -622,7 +622,7 @@ public class UserPreferences {
.apply();
// when updating with an interval, we assume the user wants
// to update *now* and then every 'hours' interval thereafter.
restartUpdateAlarm();
AutoUpdateManager.restartUpdateAlarm();
}
/**
@ -632,7 +632,7 @@ public class UserPreferences {
prefs.edit()
.putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute)
.apply();
restartUpdateAlarm();
AutoUpdateManager.restartUpdateAlarm();
}
public static void disableAutoUpdate() {
@ -882,19 +882,6 @@ public class UserPreferences {
return getUpdateTimeOfDay().length == 2;
}
public static void restartUpdateAlarm() {
if (isAutoUpdateDisabled()) {
AutoUpdateManager.disableAutoUpdate();
} else if (isAutoUpdateTimeOfDay()) {
int[] timeOfDay = getUpdateTimeOfDay();
Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay));
AutoUpdateManager.restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
} else {
long milliseconds = getUpdateInterval();
AutoUpdateManager.restartUpdateIntervalAlarm(milliseconds);
}
}
/**
* Evaluates whether Cast support (Chromecast, Audio Cast, etc) is enabled on the preferences.
*/

View File

@ -6,8 +6,7 @@ import android.content.Intent;
import android.util.Log;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.FeedUpdateUtils;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
/**
* Refreshes all feeds when it receives an intent
@ -20,7 +19,8 @@ public class FeedUpdateReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received intent");
ClientConfig.initialize(context);
FeedUpdateUtils.startAutoUpdate(context, null);
AutoUpdateManager.runOnce();
}
}

View File

@ -2,17 +2,23 @@ package de.danoeh.antennapod.core.service;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.FeedUpdateUtils;
import org.awaitility.Awaitility;
import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
public class FeedUpdateWorker extends Worker {
private static final String TAG = "FeedUpdateWorker";
public static final String PARAM_RUN_ONCE = "runOnce";
public FeedUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@ -20,16 +26,20 @@ public class FeedUpdateWorker extends Worker {
@Override
@NonNull
public Result doWork() {
final boolean isRunOnce = getInputData().getBoolean(PARAM_RUN_ONCE, false);
Log.d(TAG, "doWork() : isRunOnce = " + isRunOnce);
ClientConfig.initialize(getApplicationContext());
AtomicBoolean finished = new AtomicBoolean(false);
FeedUpdateUtils.startAutoUpdate(getApplicationContext(), () -> finished.set(true));
Awaitility.await().until(finished::get);
if (NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed()) {
DBTasks.refreshAllFeeds(getApplicationContext());
} else {
Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
}
if (UserPreferences.isAutoUpdateTimeOfDay()) {
if (!isRunOnce && UserPreferences.isAutoUpdateTimeOfDay()) {
// WorkManager does not allow to set specific time for repeated tasks.
// We repeatedly schedule a OneTimeWorkRequest instead.
UserPreferences.restartUpdateAlarm();
AutoUpdateManager.restartUpdateAlarm();
}
return Result.success();

View File

@ -13,6 +13,8 @@ import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
@ -190,10 +192,8 @@ public class DownloadService extends Service {
handleFailedDownload(status, downloader.getDownloadRequest());
if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
long id = status.getFeedfileId();
FeedMedia media = DBReader.getFeedMedia(id);
FeedItem item;
if (media == null || (item = media.getItem()) == null) {
FeedItem item = getFeedItemFromId(status.getFeedfileId());
if (item == null) {
return;
}
boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
@ -213,9 +213,8 @@ public class DownloadService extends Service {
// if FeedMedia download has been canceled, fake FeedItem update
// so that lists reload that it
if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId());
FeedItem item;
if (media == null || (item = media.getItem()) == null) {
FeedItem item = getFeedItemFromId(status.getFeedfileId());
if (item == null) {
return;
}
EventBus.getDefault().post(FeedItemEvent.updated(item));
@ -386,6 +385,12 @@ public class DownloadService extends Service {
Downloader d = getDownloader(url);
if (d != null) {
d.cancel();
DownloadRequester.getInstance().removeDownload(d.getDownloadRequest());
FeedItem item = getFeedItemFromId(d.getDownloadRequest().getFeedfileId());
if (item != null) {
EventBus.getDefault().post(FeedItemEvent.updated(item));
}
} else {
Log.e(TAG, "Could not cancel download with url " + url);
}
@ -430,12 +435,40 @@ public class DownloadService extends Service {
queryDownloads();
}
private Downloader getDownloader(DownloadRequest request) {
if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) {
Log.e(TAG, "Could not find appropriate downloader for " + request.getSource());
return null;
@VisibleForTesting
public interface DownloaderFactory {
@Nullable
Downloader create(@NonNull DownloadRequest request);
}
private static class DefaultDownloaderFactory implements DownloaderFactory {
@Nullable
@Override
public Downloader create(@NonNull DownloadRequest request) {
if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) {
Log.e(TAG, "Could not find appropriate downloader for " + request.getSource());
return null;
}
return new HttpDownloader(request);
}
return new HttpDownloader(request);
}
private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory();
@VisibleForTesting
public static DownloaderFactory getDownloaderFactory() {
return downloaderFactory;
}
// public scope rather than package private,
// because androidTest put classes in the non-standard de.test.antennapod hierarchy
@VisibleForTesting
public static void setDownloaderFactory(DownloaderFactory downloaderFactory) {
DownloadService.downloaderFactory = downloaderFactory;
}
private Downloader getDownloader(@NonNull DownloadRequest request) {
return downloaderFactory.create(request);
}
/**
@ -578,6 +611,16 @@ public class DownloadService extends Service {
syncExecutor.execute(new FailedDownloadHandler(status, request));
}
@Nullable
private FeedItem getFeedItemFromId(long id) {
FeedMedia media = DBReader.getFeedMedia(id);
if (media != null) {
return media.getItem();
} else {
return null;
}
}
/**
* Takes a single Feed, parses the corresponding file and refreshes
* information in the manager
@ -654,6 +697,7 @@ public class DownloadService extends Service {
Log.e(TAG, "FeedSyncThread was interrupted");
} catch (ExecutionException e) {
Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage());
e.printStackTrace();
}
}
@ -688,6 +732,7 @@ public class DownloadService extends Service {
Log.e(TAG, "FeedSyncThread was interrupted");
} catch (ExecutionException e) {
Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage());
e.printStackTrace();
}
}
@ -983,14 +1028,17 @@ public class DownloadService extends Service {
final FeedItem item = media.getItem();
try {
DBWriter.setFeedMedia(media).get();
// we've received the media, we don't want to autodownload it again
if (item != null) {
item.setAutoDownload(false);
// setFeedItem() signals (via EventBus) that the item has been updated,
// so we do it after the enclosing media has been updated above,
// to ensure subscribers will get the updated FeedMedia as well
DBWriter.setFeedItem(item).get();
}
DBWriter.setFeedMedia(media).get();
if (item != null && UserPreferences.enqueueDownloadedEpisodes() &&
!DBTasks.isInQueue(DownloadService.this, item.getId())) {
DBWriter.addQueueItem(DownloadService.this, item).get();
@ -1058,7 +1106,13 @@ public class DownloadService extends Service {
private final Runnable postDownloaderTask = new Runnable() {
@Override
public void run() {
List<Downloader> list = Collections.unmodifiableList(downloads);
List<Downloader> runningDownloads = new ArrayList<>();
for (Downloader downloader : downloads) {
if (!downloader.cancelled) {
runningDownloads.add(downloader);
}
}
List<Downloader> list = Collections.unmodifiableList(runningDownloads);
EventBus.getDefault().postSticky(DownloadEvent.refresh(list));
postHandler.postDelayed(postDownloaderTask, 1500);
}
@ -1076,6 +1130,9 @@ public class DownloadService extends Service {
private static String compileNotificationString(List<Downloader> downloads) {
List<String> lines = new ArrayList<>(downloads.size());
for (Downloader downloader : downloads) {
if (downloader.cancelled) {
continue;
}
StringBuilder line = new StringBuilder("");
DownloadRequest request = downloader.getDownloadRequest();
switch (request.getFeedfileType()) {

View File

@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.view.SurfaceHolder;
import com.google.android.exoplayer2.C;
@ -23,38 +23,42 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import org.antennapod.audio.MediaPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import java.util.concurrent.TimeUnit;
public class ExoPlayerWrapper implements IPlayer {
private static final String TAG = "ExoPlayerWrapper";
private final Context mContext;
private final Disposable bufferingUpdateDisposable;
private SimpleExoPlayer mExoPlayer;
private MediaSource mediaSource;
private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener;
private MediaPlayer.OnCompletionListener audioCompletionListener;
private MediaPlayer.OnErrorListener audioErrorListener;
private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener;
private boolean shouldCheckBufferingUpdates = true;
ExoPlayerWrapper(Context context) {
mContext = context;
mExoPlayer = createPlayer();
Handler handler = new Handler(); // Main thread
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (bufferingUpdateListener != null) {
bufferingUpdateListener.onBufferingUpdate(null, mExoPlayer.getBufferedPercentage());
}
if (shouldCheckBufferingUpdates) {
handler.postDelayed(this, 2000);
}
}
}, 2000);
bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aLong -> {
if (bufferingUpdateListener != null) {
bufferingUpdateListener.onBufferingUpdate(null, mExoPlayer.getBufferedPercentage());
}
});
}
private SimpleExoPlayer createPlayer() {
@ -62,6 +66,7 @@ public class ExoPlayerWrapper implements IPlayer {
loadControl.setBufferDurationsMs(30000, 120000,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true);
SimpleExoPlayer p = ExoPlayerFactory.newSimpleInstance(mContext, new DefaultRenderersFactory(mContext),
new DefaultTrackSelector(), loadControl.createDefaultLoadControl());
p.setSeekParameters(SeekParameters.PREVIOUS_SYNC);
@ -168,7 +173,7 @@ public class ExoPlayerWrapper implements IPlayer {
@Override
public void release() {
shouldCheckBufferingUpdates = false;
bufferingUpdateDisposable.dispose();
if (mExoPlayer != null) {
mExoPlayer.release();
}
@ -202,8 +207,13 @@ public class ExoPlayerWrapper implements IPlayer {
@Override
public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException {
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, mContext.getPackageName()), null);
Log.d(TAG, "setDataSource: " + s);
DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
Util.getUserAgent(mContext, mContext.getPackageName()), null,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(mContext, null, httpDataSourceFactory);
ExtractorMediaSource.Factory f = new ExtractorMediaSource.Factory(dataSourceFactory);
mediaSource = f.createMediaSource(Uri.parse(s));
}

View File

@ -584,7 +584,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
playerLock.unlock();
Log.d(TAG, "getPosition() -> " + retVal);
return retVal;
}

View File

@ -12,7 +12,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
@ -45,10 +44,12 @@ import com.bumptech.glide.request.target.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
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.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
@ -66,7 +67,6 @@ 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.IntentUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.QueueAccess;
@ -74,6 +74,9 @@ import de.danoeh.antennapod.core.util.gui.NotificationUtils;
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 io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import org.greenrobot.eventbus.EventBus;
/**
@ -212,6 +215,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private PlaybackServiceTaskManager taskManager;
private PlaybackServiceFlavorHelper flavorHelper;
private PlaybackServiceStateManager stateManager;
private Disposable positionEventTimer;
/**
* Used for Lollipop notifications, Android Wear, and Android Auto.
@ -331,8 +335,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
isRunning = false;
currentMediaType = MediaType.UNKNOWN;
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(prefListener);
cancelPositionObserver();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
if (mediaSession != null) {
mediaSession.release();
}
@ -451,7 +455,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
notificationBuilder.loadIcon(getPlayable());
}
}
startForeground(NOTIFICATION_ID, notificationBuilder.build());
stateManager.startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
@ -566,7 +570,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if (status == PlayerStatus.PLAYING) {
mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
mediaPlayer.pause(!UserPreferences.isPersistNotify(), false);
} else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
mediaPlayer.resume();
} else if (status == PlayerStatus.PREPARING) {
@ -590,7 +594,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return true;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
if (status == PlayerStatus.PLAYING) {
mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
mediaPlayer.pause(!UserPreferences.isPersistNotify(), false);
}
return true;
@ -717,7 +721,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
updateMediaSession(newInfo.playerStatus);
switch (newInfo.playerStatus) {
case INITIALIZED:
writePlaybackPreferences();
PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable,
mediaPlayer.getPSMPInfo().playerStatus, mediaPlayer.isStreaming());
break;
case PREPARED:
@ -734,7 +739,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
// remove notification on pause
stateManager.stopForeground(true);
}
writePlayerStatusPlaybackPreferences();
cancelPositionObserver();
PlaybackPreferences.writePlayerStatus(mediaPlayer.getPlayerStatus());
break;
case STOPPED:
@ -743,8 +749,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
break;
case PLAYING:
writePlayerStatusPlaybackPreferences();
PlaybackPreferences.writePlayerStatus(mediaPlayer.getPlayerStatus());
setupNotification(newInfo);
setupPositionUpdater();
stateManager.validStartCommandWasReceived();
// set sleep timer if auto-enabled
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING &&
@ -1023,82 +1030,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_disabled_label)));
}
private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) {
int playerStatusAsInt;
switch (playerStatus) {
case PLAYING:
playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING;
break;
case PAUSED:
playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED;
break;
default:
playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER;
}
return playerStatusAsInt;
}
private void writePlaybackPreferences() {
Log.d(TAG, "Writing playback preferences");
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).edit();
PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
MediaType mediaType = mediaPlayer.getCurrentMediaType();
boolean stream = mediaPlayer.isStreaming();
int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus);
if (info.playable != null) {
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
info.playable.getPlayableType());
editor.putBoolean(
PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM,
stream);
editor.putBoolean(
PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO,
mediaType == MediaType.VIDEO);
if (info.playable instanceof FeedMedia) {
FeedMedia fMedia = (FeedMedia) info.playable;
editor.putLong(
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
fMedia.getItem().getFeed().getId());
editor.putLong(
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
fMedia.getId());
} else {
editor.putLong(
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
PlaybackPreferences.NO_MEDIA_PLAYING);
editor.putLong(
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
PlaybackPreferences.NO_MEDIA_PLAYING);
}
info.playable.writeToPreferences(editor);
} else {
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA,
PlaybackPreferences.NO_MEDIA_PLAYING);
editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID,
PlaybackPreferences.NO_MEDIA_PLAYING);
editor.putLong(
PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID,
PlaybackPreferences.NO_MEDIA_PLAYING);
}
editor.putInt(
PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus);
editor.apply();
}
private void writePlayerStatusPlaybackPreferences() {
Log.d(TAG, "Writing player status playback preferences");
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).edit();
int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus());
editor.putInt(PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus);
editor.apply();
}
private void sendNotificationBroadcast(int type, int code) {
Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
@ -1653,6 +1584,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return mediaPlayer.getVideoSize();
}
private void setupPositionUpdater() {
if (positionEventTimer != null) {
positionEventTimer.dispose();
}
Log.d(TAG, "Setting up position observer");
positionEventTimer = Observable.interval(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aLong ->
EventBus.getDefault().post(new PlaybackPositionEvent(getCurrentPosition(), getDuration())));
}
private void cancelPositionObserver() {
if (positionEventTimer != null) {
positionEventTimer.dispose();
}
}
private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() {
private static final String TAG = "MediaSessionCompat";

View File

@ -141,7 +141,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build
stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
addAction(R.drawable.ic_media_cast_disconnect,
addAction(R.drawable.ic_notification_cast_off,
context.getString(R.string.cast_disconnect_label),
stopCastingPendingIntent);
numActions++;
@ -150,7 +150,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
addAction(android.R.drawable.ic_media_rew, context.getString(R.string.rewind_label), rewindButtonPendingIntent);
addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), rewindButtonPendingIntent);
if (UserPreferences.showRewindOnCompactNotification()) {
compactActionList.add(numActions);
}
@ -159,14 +159,14 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build
if (playerStatus == PlayerStatus.PLAYING) {
PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PAUSE, numActions);
addAction(android.R.drawable.ic_media_pause, //pause action
addAction(R.drawable.ic_notification_pause, //pause action
context.getString(R.string.pause_label),
pauseButtonPendingIntent);
compactActionList.add(numActions++);
} else {
PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
addAction(android.R.drawable.ic_media_play, //play action
addAction(R.drawable.ic_notification_play, //play action
context.getString(R.string.play_label),
playButtonPendingIntent);
compactActionList.add(numActions++);
@ -175,7 +175,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build
// ff follows play, then we have skip (if it's present)
PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions);
addAction(android.R.drawable.ic_media_ff, context.getString(R.string.fast_forward_label), ffButtonPendingIntent);
addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), ffButtonPendingIntent);
if (UserPreferences.showFastForwardOnCompactNotification()) {
compactActionList.add(numActions);
}
@ -184,7 +184,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build
if (UserPreferences.isFollowQueue()) {
PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_NEXT, numActions);
addAction(android.R.drawable.ic_media_next,
addAction(R.drawable.ic_notification_skip,
context.getString(R.string.skip_episode_label),
skipButtonPendingIntent);
if (UserPreferences.showSkipOnCompactNotification()) {

View File

@ -7,6 +7,9 @@ import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.util.Log;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -14,13 +17,12 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.Playable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@ -102,6 +104,34 @@ public class PlaybackServiceTaskManager {
}
}
@Subscribe
public void onEvent(FeedItemEvent event) {
// Use case: when an item in the queue has been downloaded,
// listening to the event to ensure the downloaded item will be used.
Log.d(TAG, "onEvent(FeedItemEvent " + event + ")");
for (FeedItem item : event.items) {
if (isItemInQueue(item.getId())) {
Log.d(TAG, "onEvent(FeedItemEvent) - some item (" + item.getId() + ") in the queue has been updated (usually downloaded). Refresh the queue.");
cancelQueueLoader();
loadQueue();
return;
}
}
}
private boolean isItemInQueue(long itemId) {
List<FeedItem> queue = getQueueIfLoaded();
if (queue != null) {
for (FeedItem item : queue) {
if (item.getId() == itemId) {
return true;
}
}
}
return false;
}
/**
* Returns the queue if it is already loaded or null if it hasn't been loaded yet.
* In order to wait until the queue has been loaded, use getQueue()

View File

@ -3,7 +3,8 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.support.annotation.Nullable;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
@ -144,53 +145,36 @@ public final class DBTasks {
private static final AtomicBoolean isRefreshing = new AtomicBoolean(false);
/**
* Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still
* Refreshes all feeds.
* It must not be from the main 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.
*/
public static void refreshAllFeeds(final Context context, final List<Feed> feeds) {
refreshAllFeeds(context, feeds, null);
}
/**
* 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) {
public static void refreshAllFeeds(final Context context) {
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);
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new IllegalStateException("DBTasks.refreshAllFeeds() must not be called from the main thread.");
}
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
refreshFeeds(context, DBReader.getFeedList());
isRefreshing.set(false);
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
// Note: automatic download of episodes will be done but not here.
// Instead it is done after all feeds have been refreshed (asynchronously),
// in DownloadService.onDestroy()
// See Issue #2577 for the details of the rationale
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply();
if (callback != null) {
callback.run();
}
}).start();
if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) {
GpodnetSyncService.sendSyncIntent(context);
}
// Note: automatic download of episodes will be done but not here.
// Instead it is done after all feeds have been refreshed (asynchronously),
// in DownloadService.onDestroy()
// See Issue #2577 for the details of the rationale
}
/**
@ -457,10 +441,9 @@ public final class DBTasks {
/**
* Get a FeedItem by its identifying value.
*/
private static FeedItem searchFeedItemByIdentifyingValue(Feed feed,
String identifier) {
private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, String identifier) {
for (FeedItem item : feed.getItems()) {
if (item.getIdentifyingValue().equals(identifier)) {
if (TextUtils.equals(item.getIdentifyingValue(), identifier)) {
return item;
}
}

View File

@ -28,8 +28,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.ProgressEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
@ -38,7 +36,6 @@ import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.util.LongIntMap;
import org.greenrobot.eventbus.EventBus;
// TODO Remove media column from feeditem table
@ -1478,11 +1475,9 @@ public class PodDBAdapter {
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion) {
EventBus.getDefault().post(ProgressEvent.start(context.getString(R.string.progress_upgrading_database)));
Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to "
+ newVersion + ".");
DBUpgrader.upgrade(db, oldVersion, newVersion);
EventBus.getDefault().post(ProgressEvent.end());
}
}
}

View File

@ -1,36 +0,0 @@
package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.util.Log;
import org.awaitility.core.ConditionTimeoutException;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.storage.DBTasks;
import static org.awaitility.Awaitility.with;
public class FeedUpdateUtils {
private static final String TAG = "FeedUpdateUtils";
private FeedUpdateUtils() {}
public static void startAutoUpdate(Context context, Runnable callback) {
// the network check is blocking for possibly a long time: so run the logic
// in a separate thread to prevent the code blocking the callers
final Runnable runnable = () -> {
try {
with().pollInterval(1, TimeUnit.SECONDS)
.await()
.atMost(10, TimeUnit.SECONDS)
.until(() -> NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed());
DBTasks.refreshAllFeeds(context, null, callback);
} catch (ConditionTimeoutException ignore) {
Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed");
}
};
new Thread(runnable).start();
}
}

View File

@ -1,13 +1,20 @@
package de.danoeh.antennapod.core.util;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import de.danoeh.antennapod.core.R;
import java.util.List;
public class IntentUtils {
private static final String TAG = "IntentUtils";
private IntentUtils(){}
/*
@ -28,4 +35,13 @@ public class IntentUtils {
context.sendBroadcast(new Intent(action).setPackage(context.getPackageName()));
}
public static void openInBrowser(Context context, String url) {
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
context.startActivity(myIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.pref_no_browser_found, Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}
}

View File

@ -77,7 +77,7 @@ public final class Optional<T> {
* @param <T> Type of the non-existent value
* @return an empty {@code Optional}
*/
public static<T> Optional<T> empty() {
public static <T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;

View File

@ -164,7 +164,7 @@ public class QueueSorter {
Collections.sort(feedItems, itemComparator);
if (spread == 0) {
spread = feedItems.size();
} else if (feedItems.size() % spread != 0){
} else if (spread % feedItems.size() != 0){
spread *= feedItems.size();
}
}
@ -180,6 +180,9 @@ public class QueueSorter {
Map<Long, List<FeedItem>> spreadItems = new HashMap<>();
for (List<FeedItem> feedItems : feeds) {
long thisSpread = spread / feedItems.size();
if (thisSpread == 0) {
thisSpread = 1;
}
// Starting from 0 ensures we front-load, so the queue starts with one episode from
// each feed in the queue
long itemSpread = 0;

View File

@ -1,31 +1,55 @@
package de.danoeh.antennapod.core.util.download;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.FeedUpdateWorker;
import java.util.Arrays;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.FeedUpdateWorker;
import de.danoeh.antennapod.core.storage.DBTasks;
public class AutoUpdateManager {
private static final String WORK_ID_FEED_UPDATE = FeedUpdateWorker.class.getName();
private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker";
private static final String WORK_ID_FEED_UPDATE_ONCE = WORK_ID_FEED_UPDATE + "Once";
private static final String TAG = "AutoUpdateManager";
private AutoUpdateManager() {
}
/**
* Start / restart periodic auto feed refresh
*/
public static void restartUpdateAlarm() {
if (UserPreferences.isAutoUpdateDisabled()) {
disableAutoUpdate();
} else if (UserPreferences.isAutoUpdateTimeOfDay()) {
int[] timeOfDay = UserPreferences.getUpdateTimeOfDay();
Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay));
restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]);
} else {
long milliseconds = UserPreferences.getUpdateInterval();
restartUpdateIntervalAlarm(milliseconds);
}
}
/**
* Sets the interval in which the feeds are refreshed automatically
*/
public static void restartUpdateIntervalAlarm(long intervalMillis) {
private static void restartUpdateIntervalAlarm(long intervalMillis) {
Log.d(TAG, "Restarting update alarm.");
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(FeedUpdateWorker.class,
@ -40,7 +64,7 @@ public class AutoUpdateManager {
/**
* Sets time of day the feeds are refreshed automatically
*/
public static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) {
private static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) {
Log.d(TAG, "Restarting update alarm.");
Calendar now = Calendar.getInstance();
@ -60,6 +84,41 @@ public class AutoUpdateManager {
WorkManager.getInstance().enqueueUniqueWork(WORK_ID_FEED_UPDATE, ExistingWorkPolicy.REPLACE, workRequest);
}
/**
* Run auto feed refresh once in background, as soon as what OS scheduling allows.
*
* Callers from UI should use {@link #runImmediate(Context)}, as it will guarantee
* the refresh be run immediately.
*/
public static void runOnce() {
Log.d(TAG, "Run auto update once, as soon as OS allows.");
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class)
.setConstraints(getConstraints())
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
.setInputData(new Data.Builder()
.putBoolean(FeedUpdateWorker.PARAM_RUN_ONCE, true)
.build()
)
.build();
WorkManager.getInstance().enqueueUniqueWork(WORK_ID_FEED_UPDATE_ONCE, ExistingWorkPolicy.REPLACE, workRequest);
}
/**
/**
* Run auto feed refresh once in background immediately, using its own thread.
*
* Callers where the additional threads is not suitable should use {@link #runOnce()}
*/
public static void runImmediate(@NonNull Context context) {
Log.d(TAG, "Run auto update immediately in background.");
new Thread(() -> {
DBTasks.refreshAllFeeds(context.getApplicationContext());
}, "ManualRefreshAllFeeds").start();
}
public static void disableAutoUpdate() {
WorkManager.getInstance().cancelUniqueWork(WORK_ID_FEED_UPDATE);
}
@ -74,4 +133,5 @@ public class AutoUpdateManager {
}
return constraints.build();
}
}

View File

@ -1,12 +1,9 @@
package de.danoeh.antennapod.core.util.exception;
import android.util.Log;
import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException;
import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.plugins.RxJavaPlugins;
import java.io.InterruptedIOException;
public class RxJavaErrorHandlerSetup {
private RxJavaErrorHandlerSetup() {
@ -14,21 +11,14 @@ public class RxJavaErrorHandlerSetup {
}
public static void setupRxJavaErrorHandler() {
RxJavaPlugins.setErrorHandler(originalCause -> {
Throwable e = originalCause;
RxJavaPlugins.setErrorHandler(e -> {
if (e instanceof UndeliverableException) {
e = e.getCause();
}
if (e instanceof GpodnetServiceException) {
e = e.getCause();
}
if (e instanceof InterruptedException || e instanceof InterruptedIOException) {
// fine, some blocking code was interrupted by a dispose call
Log.d("RxJavaErrorHandler", "Ignored exception: " + Log.getStackTraceString(originalCause));
// Probably just disposed because the fragment was left
Log.d("RxJavaErrorHandler", "Ignored exception: " + Log.getStackTraceString(e));
return;
}
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), originalCause);
.uncaughtException(Thread.currentThread(), e);
});
}
}

View File

@ -72,7 +72,7 @@ public class ChapterReader extends ID3Reader {
String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
currentChapter.setLink(decodedLink);
Log.d(TAG, "Found link: " + currentChapter.getLink());
} catch (IllegalArgumentException _iae) {
} catch (IllegalArgumentException iae) {
Log.w(TAG, "Bad URL found in ID3 data");
}

View File

@ -21,11 +21,10 @@ import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedMedia;
@ -69,9 +68,6 @@ public class PlaybackController {
private final ScheduledThreadPoolExecutor schedExecutor;
private static final int SCHED_EX_POOLSIZE = 1;
private MediaPositionObserver positionObserver;
private ScheduledFuture<?> positionObserverFuture;
private boolean mediaInfoLoaded = false;
private boolean released = false;
private boolean initialized = false;
@ -177,7 +173,6 @@ public class PlaybackController {
} catch (IllegalArgumentException e) {
// ignore
}
cancelPositionObserver();
schedExecutor.shutdownNow();
media = null;
released = true;
@ -254,29 +249,6 @@ public class PlaybackController {
.getIntent());
}
private void setupPositionObserver() {
if (positionObserverFuture == null ||
positionObserverFuture.isCancelled() ||
positionObserverFuture.isDone()) {
Log.d(TAG, "Setting up position observer");
positionObserver = new MediaPositionObserver();
positionObserverFuture = schedExecutor.scheduleWithFixedDelay(
positionObserver, MediaPositionObserver.WAITING_INTERVALL,
MediaPositionObserver.WAITING_INTERVALL,
TimeUnit.MILLISECONDS);
}
}
private void cancelPositionObserver() {
if (positionObserverFuture != null) {
boolean result = positionObserverFuture.cancel(true);
Log.d(TAG, "PositionObserver cancelled. Result: " + result);
}
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
if(service instanceof PlaybackService.LocalBinder) {
@ -337,7 +309,6 @@ public class PlaybackController {
onBufferUpdate(progress);
break;
case PlaybackService.NOTIFICATION_TYPE_RELOAD:
cancelPositionObserver();
mediaInfoLoaded = false;
queryService();
onReloadNotification(intent.getIntExtra(
@ -447,7 +418,6 @@ public class PlaybackController {
case PAUSED:
clearStatusMsg();
checkMediaInfoLoaded();
cancelPositionObserver();
onPositionObserverUpdate();
updatePlayButtonAppearance(playResource, playText);
if (!PlaybackService.isCasting() &&
@ -463,7 +433,6 @@ public class PlaybackController {
onAwaitingVideoSurface();
setScreenOn(true);
}
setupPositionObserver();
updatePlayButtonAppearance(pauseResource, pauseText);
break;
case PREPARING:
@ -581,7 +550,6 @@ public class PlaybackController {
*/
public void onSeekBarStartTrackingTouch(SeekBar seekBar) {
// interrupt position Observer, restart later
cancelPositionObserver();
}
/**
@ -590,7 +558,6 @@ public class PlaybackController {
public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) {
if (playbackService != null && media != null) {
playbackService.seekTo((int) (prog * media.getDuration()));
setupPositionObserver();
}
}
@ -836,19 +803,4 @@ public class PlaybackController {
}
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
/**
* Refreshes the current position of the media file that is playing.
*/
public class MediaPositionObserver implements Runnable {
static final int WAITING_INTERVALL = 1000;
@Override
public void run() {
if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) {
activity.runOnUiThread(PlaybackController.this::onPositionObserverUpdate);
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M1.6,1.27L0.25,2.75L1.41,3.8C1.16,4.13 1,4.55 1,5V8H3V5.23L18.2,19H14V21H20.41L22.31,22.72L23.65,21.24M6.5,3L8.7,5H21V16.14L23,17.95V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.08,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" />
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
</vector>

View File

@ -1,5 +1,6 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FF0000"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View File

@ -394,6 +394,8 @@
<string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string>
<string name="pref_autodl_wifi_filter_title">Enable Wi-Fi filter</string>
<string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string>
<string name="autodl_wifi_filter_permission_title">Permission required</string>
<string name="autodl_wifi_filter_permission_message">Location permission is required for Wi-Fi filter. Tap to grant the permission.</string>
<string name="pref_automatic_download_on_battery_title">Download when not charging</string>
<string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string>
<string name="pref_parallel_downloads_title">Parallel Downloads</string>

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.core;
import android.content.Context;
import android.util.Log;
import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
@ -15,6 +16,8 @@ import de.danoeh.antennapod.core.util.exception.RxJavaErrorHandlerSetup;
* Apps using the core module of AntennaPod should register implementations of all interfaces here.
*/
public class ClientConfig {
private static final String TAG = "ClientConfig";
private ClientConfig(){}
/**
@ -44,7 +47,15 @@ public class ClientConfig {
UserPreferences.init(context);
PlaybackPreferences.init(context);
NetworkUtils.init(context);
CastManager.init(context);
// Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
// Google Play Service usage.
// Down side: when the user decides to enable casting, AntennaPod needs to be restarted
// for it to take effect.
if (UserPreferences.isCastEnabled()) {
CastManager.init(context);
} else {
Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped.");
}
SleepTimerPreferences.init(context);
RxJavaErrorHandlerSetup.setupRxJavaErrorHandler();
initialized = true;

View File

@ -163,6 +163,10 @@ public class CastManager extends BaseCastManager implements OnFailedListener {
return INSTANCE;
}
public static boolean isInitialized() {
return INSTANCE != null;
}
/**
* Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media
* control APIs that this library do not provide a wrapper for, client applications can call

View File

@ -56,11 +56,18 @@ public class PlaybackServiceFlavorHelper {
PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
this.callback = callback;
if (!CastManager.isInitialized()) {
return;
}
mediaRouter = MediaRouter.getInstance(context.getApplicationContext());
setCastConsumer(context);
}
void initializeMediaPlayer(Context context) {
if (!CastManager.isInitialized()) {
callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
return;
}
castManager = CastManager.getInstance();
castManager.addCastConsumer(castConsumer);
boolean isCasting = castManager.isConnected();
@ -77,10 +84,16 @@ public class PlaybackServiceFlavorHelper {
}
void removeCastConsumer() {
if (!CastManager.isInitialized()) {
return;
}
castManager.removeCastConsumer(castConsumer);
}
boolean castDisconnect(boolean castDisconnect) {
if (!CastManager.isInitialized()) {
return false;
}
if (castDisconnect) {
castManager.disconnect();
}
@ -88,6 +101,9 @@ public class PlaybackServiceFlavorHelper {
}
boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
if (!CastManager.isInitialized()) {
return false;
}
switch (code) {
case RemotePSMP.CAST_ERROR:
callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST, resourceId);
@ -218,6 +234,9 @@ public class PlaybackServiceFlavorHelper {
}
void registerWifiBroadcastReceiver() {
if (!CastManager.isInitialized()) {
return;
}
if (wifiBroadcastReceiver != null) {
return;
}
@ -243,6 +262,9 @@ public class PlaybackServiceFlavorHelper {
}
void unregisterWifiBroadcastReceiver() {
if (!CastManager.isInitialized()) {
return;
}
if (wifiBroadcastReceiver != null) {
callback.unregisterReceiver(wifiBroadcastReceiver);
wifiBroadcastReceiver = null;
@ -250,6 +272,9 @@ public class PlaybackServiceFlavorHelper {
}
boolean onSharedPreference(String key) {
if (!CastManager.isInitialized()) {
return false;
}
if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
if (!UserPreferences.isCastEnabled()) {
if (castManager.isConnecting() || castManager.isConnected()) {
@ -263,6 +288,9 @@ public class PlaybackServiceFlavorHelper {
}
void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
if (!CastManager.isInitialized()) {
return;
}
PlaybackStateCompat.CustomAction.Builder actionBuilder =
new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
Bundle actionExtras = new Bundle();
@ -273,6 +301,9 @@ public class PlaybackServiceFlavorHelper {
}
void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
if (!CastManager.isInitialized()) {
return;
}
Bundle sessionExtras = new Bundle();
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);