diff options
30 files changed, 711 insertions, 198 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 78cc15b2c..fa5012b74 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -47,6 +47,7 @@ public class VideoplayerActivity extends MediaplayerActivity { */ private boolean videoControlsShowing = true; private boolean videoSurfaceCreated = false; + private boolean playbackStoppedUponExitVideo = false; private boolean destroyingDueToReload = false; private VideoControlsHider videoControlsHider = new VideoControlsHider(this); @@ -77,6 +78,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onResume() { super.onResume(); + playbackStoppedUponExitVideo = false; if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { playExternalMedia(getIntent(), MediaType.VIDEO); } else if (PlaybackService.isCasting()) { @@ -91,12 +93,32 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onStop() { + stopPlaybackIfUserPreferencesSpecified(); // MUST be called before super.onStop(), while it still has member variable controller super.onStop(); if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { videoControlsHider.stop(); } } + void stopPlaybackIfUserPreferencesSpecified() { + // to avoid the method being called twice during leaving Videoplayer + // , which will double-pause the media + // (it is usually first called by surfaceHolderCallback.surfaceDestroyed(), + // then VideoplayerActivity.onStop() , but sometimes VideoplayerActivity.onStop() + // will first be invoked.) + if (playbackStoppedUponExitVideo) { + return; + } + playbackStoppedUponExitVideo = true; + + if (controller != null && !destroyingDueToReload + && UserPreferences.getVideoBackgroundBehavior() + != UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) { + Log.v(TAG, "stop video playback per UserPreference"); + controller.notifyVideoSurfaceAbandoned(); + } + } + @Override public void onUserLeaveHint () { if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior() @@ -275,13 +297,12 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override public void surfaceDestroyed(SurfaceHolder holder) { - Log.d(TAG, "Videosurface was destroyed"); + Log.d(TAG, "Videosurface was destroyed." ); + Log.v(TAG, " hasController=" + (controller != null) + + " , destroyingDueToReload=" + destroyingDueToReload + + " , videoBackgroundBehavior=" + UserPreferences.getVideoBackgroundBehavior()); videoSurfaceCreated = false; - if (controller != null && !destroyingDueToReload - && UserPreferences.getVideoBackgroundBehavior() - != UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) { - controller.notifyVideoSurfaceAbandoned(); - } + stopPlaybackIfUserPreferencesSpecified(); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java index 1286d9dc7..f54b9266e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.adapter; import android.content.Context; -import android.support.annotation.NonNull; -import android.content.Intent; import android.widget.Toast; import com.afollestad.materialdialogs.MaterialDialog; @@ -30,7 +28,7 @@ import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; */ public class DefaultActionButtonCallback implements ActionButtonCallback { - private static final String TAG = "DefaultActionButtonCallback"; + private static final String TAG = "DefaultActionBtnCb"; private final Context context; @@ -84,13 +82,9 @@ public class DefaultActionButtonCallback implements ActionButtonCallback { } } else { // media is downloaded if (media.isCurrentlyPlaying()) { - new PlaybackServiceStarter(context, media) - .startWhenPrepared(true) - .shouldStream(false) - .start(); IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE); } else if (media.isCurrentlyPaused()) { - new PlaybackServiceStarter(context, media) + new PlaybackServiceStarter(context, media) // need to start the service in case it's been stopped by system. .startWhenPrepared(true) .shouldStream(false) .start(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index beca6ab5c..5997d227a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -20,6 +20,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; +import android.widget.TextView; import android.widget.Toast; import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; @@ -74,6 +75,7 @@ public class AllEpisodesFragment extends Fragment { RecyclerView recyclerView; AllEpisodesRecycleAdapter listAdapter; private ProgressBar progLoading; + private View emptyView; List<FeedItem> episodes; private List<Downloader> downloaderList; @@ -331,17 +333,31 @@ public class AllEpisodesFragment extends Fragment { onFragmentLoaded(); } + emptyView = (View) root.findViewById(R.id.emptyView); + emptyView.setVisibility(View.GONE); + ((TextView)emptyView.findViewById(R.id.emptyViewTitle)).setText(R.string.no_all_episodes_head_label); + ((TextView)emptyView.findViewById(R.id.emptyViewMessage)).setText(R.string.no_all_episodes_label); + return root; } private void onFragmentLoaded() { - if (listAdapter == null) { - MainActivity mainActivity = (MainActivity) getActivity(); - listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, - new DefaultActionButtonCallback(mainActivity), showOnlyNewEpisodes()); - listAdapter.setHasStableIds(true); - recyclerView.setAdapter(listAdapter); + if (episodes != null && episodes.size() > 0) { + if (listAdapter == null) { + MainActivity mainActivity = (MainActivity) getActivity(); + listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, + new DefaultActionButtonCallback(mainActivity), showOnlyNewEpisodes()); + listAdapter.setHasStableIds(true); + recyclerView.setAdapter(listAdapter); + } + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } else { + listAdapter = null; + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.VISIBLE); } + listAdapter.notifyDataSetChanged(); restoreScrollPosition(); getActivity().supportInvalidateOptionsMenu(); @@ -473,6 +489,7 @@ public class AllEpisodesFragment extends Fragment { } if (viewsCreated && !itemsLoaded) { recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); progLoading.setVisibility(View.VISIBLE); } disposable = Observable.fromCallable(this::loadData) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index c26d7498e..b52fd444f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -21,6 +21,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -106,6 +107,11 @@ public class CompletedDownloadsFragment extends ListFragment { if (items != null && getActivity() != null) { onFragmentLoaded(); } + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setTitle(R.string.no_comp_downloads_head_label); + emptyView.setMessage(R.string.no_comp_downloads_label); + emptyView.attachToListView(getListView()); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index 9f8f59f7f..973772049 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -24,6 +24,7 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -75,6 +76,12 @@ public class DownloadLogFragment extends ListFragment { if (itemsLoaded) { onFragmentLoaded(); } + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setTitle(R.string.no_log_downloads_head_label); + emptyView.setMessage(R.string.no_log_downloads_label); + emptyView.attachToListView(getListView()); + } private void onFragmentLoaded() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java index 70f82c2ec..cda89bbd3 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import java.util.List; @@ -50,6 +51,8 @@ public class FavoriteEpisodesFragment extends AllEpisodesFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = super.onCreateViewHelper(inflater, container, savedInstanceState, R.layout.all_episodes_fragment); + ((TextView)root.findViewById(R.id.emptyViewTitle)).setText(R.string.no_fav_episodes_head_label); + ((TextView)root.findViewById(R.id.emptyViewMessage)).setText(R.string.no_fav_episodes_label); ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 4ee7a06ad..124665a2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -307,7 +307,7 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { - webvDescription.loadDataWithBaseURL(null, data, "text/html", + webvDescription.loadDataWithBaseURL("https://127.0.0.1", data, "text/html", "utf-8", "about:blank"); Log.d(TAG, "Webview loaded"); }, error -> Log.e(TAG, Log.getStackTraceString(error))); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index bcca281d4..e6e02296d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -358,7 +358,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { private void onFragmentLoaded() { if (webviewData != null) { - webvDescription.loadDataWithBaseURL(null, webviewData, "text/html", "utf-8", "about:blank"); + webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank"); } updateAppearance(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index 335ee224b..c2b61bf75 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -7,9 +7,8 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - +import android.widget.TextView; import java.util.List; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; import de.danoeh.antennapod.core.event.FeedItemEvent; @@ -26,9 +25,7 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; public class NewEpisodesFragment extends AllEpisodesFragment { public static final String TAG = "NewEpisodesFragment"; - private static final String PREF_NAME = "PrefNewEpisodesFragment"; - @Override protected boolean showOnlyNewEpisodes() { return true; } @@ -49,6 +46,8 @@ public class NewEpisodesFragment extends AllEpisodesFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = super.onCreateViewHelper(inflater, container, savedInstanceState, R.layout.all_episodes_fragment); + ((TextView)root.findViewById(R.id.emptyViewTitle)).setText(R.string.no_new_episodes_head_label); + ((TextView)root.findViewById(R.id.emptyViewMessage)).setText(R.string.no_new_episodes_label); ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index c2a9200c8..bd4a43f83 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -29,6 +29,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.view.EmptyViewHandler; import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -81,6 +82,12 @@ public class PlaybackHistoryFragment extends ListFragment { if (itemsLoaded) { onFragmentLoaded(); } + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setTitle(R.string.no_history_head_label); + emptyView.setMessage(R.string.no_history_label); + emptyView.attachToListView(getListView()); + } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index d65c6ae18..0e8f2f083 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -72,7 +72,7 @@ public class QueueFragment extends Fragment { private TextView infoBar; private RecyclerView recyclerView; private QueueRecyclerAdapter recyclerAdapter; - private TextView txtvEmpty; + private View emptyView; private ProgressBar progLoading; private List<FeedItem> queue; @@ -493,9 +493,12 @@ public class QueueFragment extends Fragment { } ); itemTouchHelper.attachToRecyclerView(recyclerView); + //empty view + emptyView = (View) root.findViewById(R.id.emptyView); + emptyView.setVisibility(View.GONE); + ((TextView)emptyView.findViewById(R.id.emptyViewTitle)).setText(R.string.no_items_header_label); + ((TextView)emptyView.findViewById(R.id.emptyViewMessage)).setText(R.string.no_items_label); - txtvEmpty = root.findViewById(android.R.id.empty); - txtvEmpty.setVisibility(View.GONE); progLoading = root.findViewById(R.id.progLoading); progLoading.setVisibility(View.VISIBLE); @@ -503,19 +506,20 @@ public class QueueFragment extends Fragment { } private void onFragmentLoaded(final boolean restoreScrollPosition) { - if (recyclerAdapter == null) { - MainActivity activity = (MainActivity) getActivity(); - recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, - new DefaultActionButtonCallback(activity), itemTouchHelper); - recyclerAdapter.setHasStableIds(true); - recyclerView.setAdapter(recyclerAdapter); - } - if(queue == null || queue.size() == 0) { - recyclerView.setVisibility(View.GONE); - txtvEmpty.setVisibility(View.VISIBLE); - } else { - txtvEmpty.setVisibility(View.GONE); + if (queue != null && queue.size() > 0) { + if (recyclerAdapter == null) { + MainActivity activity = (MainActivity) getActivity(); + recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, + new DefaultActionButtonCallback(activity), itemTouchHelper); + recyclerAdapter.setHasStableIds(true); + recyclerView.setAdapter(recyclerAdapter); + } + emptyView.setVisibility(View.GONE); recyclerView.setVisibility(View.VISIBLE); + } else { + recyclerAdapter = null; + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.VISIBLE); } if (restoreScrollPosition) { @@ -628,7 +632,7 @@ public class QueueFragment extends Fragment { } if (queue == null) { recyclerView.setVisibility(View.GONE); - txtvEmpty.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); progLoading.setVisibility(View.VISIBLE); } disposable = Observable.fromCallable(DBReader::getQueue) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index 66c59b7f7..3c40b542c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -20,6 +20,7 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.view.EmptyViewHandler; import de.greenrobot.event.EventBus; /** @@ -44,6 +45,12 @@ public class RunningDownloadsFragment extends ListFragment { adapter = new DownloadlistAdapter(getActivity(), itemAccess); setListAdapter(adapter); + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setTitle(R.string.no_run_downloads_head_label); + emptyView.setMessage(R.string.no_run_downloads_label); + emptyView.attachToListView(getListView()); + } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java new file mode 100644 index 000000000..e3fd63235 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.view;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+
+public class EmptyViewHandler extends View {
+ private Activity activity;
+ private int title;
+ private int message;
+
+ public EmptyViewHandler(Context context) {
+ super(context);
+ this.setActivity((Activity) context);
+ }
+
+ public int getTitle() {
+ return title;
+ }
+
+ public void setTitle(int title) {
+ this.title = title;
+ }
+
+ public int getMessage() {
+ return message;
+ }
+
+ public void setMessage(int message) {
+ this.message = message;
+ }
+
+ public void attachToListView(ListView listView){
+
+ View emptyView = getActivity().getLayoutInflater().inflate(R.layout.empty_view_layout, null);
+ ((ViewGroup) listView.getParent()).addView(emptyView);
+ listView.setEmptyView(emptyView);
+
+ TextView tvTitle = (TextView) emptyView.findViewById(R.id.emptyViewTitle);
+ tvTitle.setText(title);
+
+ TextView tvMessage = (TextView) emptyView.findViewById(R.id.emptyViewMessage);
+ tvMessage.setText(message);
+
+ }
+
+ public Activity getActivity() {
+ return activity;
+ }
+
+ public void setActivity(Activity activity) {
+ this.activity = activity;
+ }
+}
diff --git a/app/src/main/res/layout/all_episodes_fragment.xml b/app/src/main/res/layout/all_episodes_fragment.xml index 89f900a1f..099216007 100644 --- a/app/src/main/res/layout/all_episodes_fragment.xml +++ b/app/src/main/res/layout/all_episodes_fragment.xml @@ -17,6 +17,10 @@ tools:itemCount="13" tools:listitem="@layout/new_episodes_listitem" /> + <include + android:id="@+id/emptyView" + layout="@layout/empty_view_layout"/> + <ProgressBar android:id="@+id/progLoading" android:layout_width="wrap_content" diff --git a/app/src/main/res/layout/empty_view_layout.xml b/app/src/main/res/layout/empty_view_layout.xml new file mode 100644 index 000000000..051773e51 --- /dev/null +++ b/app/src/main/res/layout/empty_view_layout.xml @@ -0,0 +1,30 @@ +<?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:gravity="center"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <TextView
+ android:id="@+id/emptyViewTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ tools:text="empty"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:paddingBottom="10dp"/>
+
+ <TextView
+ android:id="@+id/emptyViewMessage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="15sp"
+ tools:text="empty"
+ android:paddingLeft="15dp"
+ android:paddingRight="15dp"
+ android:textAlignment="center"/>
+
+</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml index 71bf16d30..cf00f2b1b 100644 --- a/app/src/main/res/layout/queue_fragment.xml +++ b/app/src/main/res/layout/queue_fragment.xml @@ -27,13 +27,9 @@ android:layout_below="@id/divider" android:scrollbars="vertical"/> - <TextView - android:id="@id/android:empty" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_centerInParent="true" - android:gravity="center" - android:text="@string/no_items_label" /> + <include + android:id="@+id/emptyView" + layout="@layout/empty_view_layout"/> <ProgressBar android:id="@+id/progLoading" diff --git a/app/src/main/res/xml/preferences_network.xml b/app/src/main/res/xml/preferences_network.xml index c37a99465..59dd0c51b 100644 --- a/app/src/main/res/xml/preferences_network.xml +++ b/app/src/main/res/xml/preferences_network.xml @@ -16,10 +16,11 @@ </PreferenceCategory> <PreferenceCategory android:title="@string/download_pref_details"> - <SwitchPreference - android:defaultValue="false" - android:enabled="true" - android:key="prefMobileUpdate" + <ListPreference + android:defaultValue="images" + android:entries="@array/mobile_update_entries" + android:entryValues="@array/mobile_update_values" + android:key="prefMobileUpdateAllowed" android:summary="@string/pref_mobileUpdate_sum" android:title="@string/pref_mobileUpdate_title"/> <de.danoeh.antennapod.preferences.NumberPickerPreference diff --git a/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java index 29e2456b2..1b4cbc0ea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/UpdateManager.java @@ -71,6 +71,11 @@ class UpdateManager { UserPreferences.setEpisodeCleanupValue(oldValueInDays * 24); } // else 0 or special negative values, no change needed } + if (oldVersionCode < 1070197) { + if (prefs.getBoolean("prefMobileUpdate", false)) { + prefs.edit().putString(UserPreferences.PREF_MOBILE_UPDATE, "everything").apply(); + } + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java index ec10b78aa..552c1b691 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java @@ -31,6 +31,6 @@ public class ApGlideModule extends AppGlideModule { @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); + registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index bd5276100..2e742e979 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -21,10 +21,8 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.NetworkUtils; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; +import okhttp3.*; +import okhttp3.internal.http.RealResponseBody; /** * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader @@ -111,10 +109,16 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { @Override public Response intercept(Chain chain) throws IOException { - if (NetworkUtils.isDownloadAllowed()) { + if (NetworkUtils.isImageAllowed()) { return chain.proceed(chain.request()); } else { - return null; + return new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(420) + .message("Policy Not Fulfilled") + .body(ResponseBody.create(null, new byte[0])) + .request(chain.request()) + .build(); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 1feecd849..805f0c1b6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -77,7 +77,7 @@ public class UserPreferences { // Network private static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"; public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; - private static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdateAllowed"; public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup"; public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; @@ -380,8 +380,16 @@ public class UserPreferences { } } + public static String getMobileUpdatesEnabled() { + return prefs.getString(PREF_MOBILE_UPDATE, "images"); + } + public static boolean isAllowMobileUpdate() { - return prefs.getBoolean(PREF_MOBILE_UPDATE, false); + return getMobileUpdatesEnabled().equals("everything"); + } + + public static boolean isAllowMobileImages() { + return isAllowMobileUpdate() || getMobileUpdatesEnabled().equals("images"); } public static int getParallelDownloads() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 827709350..7fe93a162 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -25,6 +25,7 @@ import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserServiceCompat; import android.support.v4.media.MediaDescriptionCompat; @@ -75,6 +76,11 @@ import de.greenrobot.event.EventBus; /** * Controls the MediaPlayer that plays a FeedMedia-file + * + * Callers should connect to the service with either: + * - .bindService() + * - ContextCompat.startForegroundService(), optionally with arguments, such as media to be played, in intent extras + * */ public class PlaybackService extends MediaBrowserServiceCompat { /** @@ -193,10 +199,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ public static boolean isRunning = false; /** - * Is true if service has received a valid start command. - */ - public static boolean started = false; - /** * Is true if the service was running, but paused due to headphone disconnect */ private static boolean transientPause = false; @@ -264,9 +266,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { Log.d(TAG, "Service created."); isRunning = true; - NotificationCompat.Builder notificationBuilder = createBasicNotification(); - startForeground(NOTIFICATION_ID, notificationBuilder.build()); - registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS")); registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); registerReceiver(shutdownReceiver, new IntentFilter(ACTION_SHUTDOWN_PLAYBACK_SERVICE)); @@ -344,7 +343,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { Log.d(TAG, "Service is about to be destroyed"); stopForeground(true); isRunning = false; - started = false; currentMediaType = MediaType.UNKNOWN; PreferenceManager.getDefaultSharedPreferences(this) @@ -366,11 +364,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { taskManager.shutdown(); } - private void stopService() { - stopForeground(true); - stopSelf(); - } - @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + @@ -464,37 +457,32 @@ public class PlaybackService extends MediaBrowserServiceCompat { final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null && !castDisconnect) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopService(); + // Typical cases when the service was started with no argument + // - when it is first bound, and then moved to startedState, as in <code>serviceManager.moveServiceToStartedState()</code> + // - callers (e.g., Controller) explicitly + Log.d(TAG, "PlaybackService was started with no arguments."); return Service.START_NOT_STICKY; } - if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); - stopForeground(true); - } else { - if (keycode != -1) { - Log.d(TAG, "Received media button event"); - boolean handled = handleKeycode(keycode, true); - if (!handled) { - stopService(); - return Service.START_NOT_STICKY; - } - } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { - started = true; - boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - //If the user asks to play External Media, the casting session, if on, should end. - flavorHelper.castDisconnect(playable instanceof ExternalMedia); - if (playable instanceof FeedMedia) { - playable = DBReader.getFeedMedia(((FeedMedia) playable).getId()); - } - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + if (keycode != -1) { + Log.d(TAG, "Received media button event"); + boolean handled = handleKeycode(keycode, true); + if (!handled) { + // Just silently ignores unsupported keycode. Whether the service will + // continue to run is solely dependent on whether it is playing some media. + return Service.START_NOT_STICKY; + } + } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + //If the user asks to play External Media, the casting session, if on, should end. + flavorHelper.castDisconnect(playable instanceof ExternalMedia); + if (playable instanceof FeedMedia) { + playable = DBReader.getFeedMedia(((FeedMedia) playable).getId()); } - setupNotification(playable); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); } return Service.START_NOT_STICKY; @@ -568,12 +556,23 @@ public class PlaybackService extends MediaBrowserServiceCompat { mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); return true; case KeyEvent.KEYCODE_MEDIA_STOP: + // The logic gives UI illusion of stop by removing notification + // In the UI within AntennaPod, including widgets, it is seen as PAUSE, e.g., + // users can still user on-screen widget to resume playing. if (status == PlayerStatus.PLAYING) { + // Implementation note: Use of a state in serviceManager to tell it to + // show stop state UI (i.e., stopForeground(true)) is a bit awkward. + // + // More intuitive API would be for mediaPlayer.pause() returns a Future that + // returns after pause, including the related async notification work completes. + // However, it has its own complication, that mediaPlayer.pause() does not + // really know when all the related work completes, as they are buried into + // (asynchronous) callbacks. + serviceManager.treatNextPauseAsStopOnUI(); mediaPlayer.pause(true, true); - started = false; + } else { + serviceManager.showUIForStopState(); } - - stopForeground(true); // gets rid of persistent notification return true; default: Log.d(TAG, "Unhandled key code: " + keycode); @@ -589,7 +588,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); if (playable != null) { mediaPlayer.playMediaObject(playable, false, true, true); - started = true; PlaybackService.this.updateMediaSessionMetadata(playable); } } @@ -604,10 +602,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { } public void notifyVideoSurfaceAbandoned() { + Log.v(TAG, "notifyVideoSurfaceAbandoned()"); mediaPlayer.pause(true, false); mediaPlayer.resetVideoSurface(); - setupNotification(getPlayable()); - stopForeground(!UserPreferences.isPersistNotify()); } private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { @@ -670,27 +667,15 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PAUSED: - if ((UserPreferences.isPersistNotify() || isCasting) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // do not remove notification on pause based on user pref and whether android version supports expanded notifications - // Change [Play] button to [Pause] - setupNotification(newInfo); - } else if (!UserPreferences.isPersistNotify() && !isCasting) { - // remove notification on pause - stopForeground(true); - } writePlayerStatusPlaybackPreferences(); break; case STOPPED: //writePlaybackPreferencesNoMediaPlaying(); - //stopService(); break; case PLAYING: writePlayerStatusPlaybackPreferences(); - setupNotification(newInfo); - started = true; // set sleep timer if auto-enabled if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && SleepTimerPreferences.autoEnable() && !sleepTimerActive()) { @@ -701,7 +686,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { case ERROR: writePlaybackPreferencesNoMediaPlaying(); - stopService(); break; } @@ -714,7 +698,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void shouldStop() { - stopService(); + serviceManager.stopService(); } @Override @@ -763,7 +747,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); writePlaybackPreferencesNoMediaPlaying(); - stopService(); return true; } @@ -847,9 +830,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (stopPlaying) { taskManager.cancelPositionSaver(); writePlaybackPreferencesNoMediaPlaying(); - if (!isCasting) { - stopForeground(true); - } } if (mediaType == null) { sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); @@ -1063,6 +1043,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { private void updateMediaSession(final PlayerStatus playerStatus) { PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder(); + @PlaybackStateCompat.State int state; if (playerStatus != null) { switch (playerStatus) { @@ -1126,7 +1107,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { flavorHelper.mediaSessionSetExtraForWear(mediaSession); - mediaSession.setPlaybackState(sessionState.build()); + final PlaybackStateCompat sessionStateBuilt = sessionState.build(); + mediaSession.setPlaybackState(sessionStateBuilt); + serviceManager.onPlaybackStateChange(sessionStateBuilt); } private static boolean useSkipToPreviousForRewindInLockscreen() { @@ -1180,7 +1163,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, imageLocation); } } - if (!Thread.currentThread().isInterrupted() && started) { + if (!Thread.currentThread().isInterrupted() && isStarted()) { mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0, PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT)); @@ -1203,21 +1186,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private Thread notificationSetupThread; - /** - * Prepares notification and starts the service in the foreground. - */ - private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - setupNotification(info.playable); - } - - private synchronized void setupNotification(final Playable playable) { + private synchronized void setupNotification(final Playable playable, boolean treatPauseAsStop) { if (notificationSetupThread != null) { notificationSetupThread.interrupt(); } if (playable == null) { Log.d(TAG, "setupNotification: playable is null"); - if (!started) { - stopService(); + if (!isStarted()) { + serviceManager.stopService(); } return; } @@ -1226,12 +1202,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void run() { - Log.d(TAG, "Starting background work"); + Log.d(TAG, "notificationSetupTask: Starting background work"); if (mediaPlayer == null) { Log.d(TAG, "notificationSetupTask: mediaPlayer is null"); - if (!started) { - stopService(); + if (!isStarted()) { + serviceManager.stopService(); } return; } @@ -1256,8 +1232,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { } PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); + Log.v(TAG, "notificationSetupTask: playerStatus=" + playerStatus); - if (!Thread.currentThread().isInterrupted() && started) { + if (!Thread.currentThread().isInterrupted() && isStarted()) { String contentText = playable.getEpisodeTitle(); String contentTitle = playable.getFeedTitle(); Notification notification; @@ -1349,15 +1326,33 @@ public class PlaybackService extends MediaBrowserServiceCompat { playerStatus == PlayerStatus.PREPARING || playerStatus == PlayerStatus.SEEKING || isCasting) { + Log.v(TAG, "notificationSetupTask: make service foreground"); startForeground(NOTIFICATION_ID, notification); + } else if (playerStatus == PlayerStatus.PAUSED) { + if (treatPauseAsStop) { + stopForeground(true); + } else if ((UserPreferences.isPersistNotify() || isCasting) && + android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // do not remove notification on pause based on user pref and whether android version supports expanded notifications + // Change [Play] button to [Pause] + leaveNotificationAsBackground(notification); + } else if (!UserPreferences.isPersistNotify() && !isCasting) { + // remove notification on pause + stopForeground(true); + } } else { - stopForeground(false); - NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - mNotificationManager.notify(NOTIFICATION_ID, notification); + leaveNotificationAsBackground(notification); } Log.d(TAG, "Notification set up"); } } + + private void leaveNotificationAsBackground(@NonNull Notification notification) { + stopForeground(false); + NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + mNotificationManager.notify(NOTIFICATION_ID, notification); + } + }; notificationSetupThread = new Thread(notificationSetupTask); notificationSetupThread.start(); @@ -1552,7 +1547,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - stopService(); + serviceManager.stopService(); } } @@ -1850,8 +1845,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); - void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); - MediaSessionCompat getMediaSession(); Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); @@ -1891,24 +1884,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) { - if (connected) { - PlaybackService.this.setupNotification(info); - } else { - PlayerStatus status = info.playerStatus; - if ((status == PlayerStatus.PLAYING || - status == PlayerStatus.SEEKING || - status == PlayerStatus.PREPARING || - UserPreferences.isPersistNotify()) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - PlaybackService.this.setupNotification(info); - } else if (!UserPreferences.isPersistNotify()) { - PlaybackService.this.stopForeground(true); - } - } - } - - @Override public MediaSessionCompat getMediaSession() { return PlaybackService.this.mediaSession; } @@ -1923,4 +1898,116 @@ public class PlaybackService extends MediaBrowserServiceCompat { PlaybackService.this.unregisterReceiver(receiver); } }; + + private boolean isStarted() { + return serviceManager.serviceInStartedState; + } + + /** + * The helper that manages PlaybackService's foreground service life cycle and the associated + * notification control. + * + * The logic is adapted from a sample app from The Android Open Source Project. + * See https://github.com/googlesamples/android-MediaBrowserService/blob/6cf01be9ef82ca2dd653f03e2a4af0b075cc0021/Application/src/main/java/com/example/android/mediasession/service/MusicService.java#L211 + * + */ + private class ServiceManager { + private boolean serviceInStartedState; + private boolean toTreatNextPauseAsStopOnUI = false; + + /** + * + * Entry point method for callers. Upon PlaybackState changes, + * the manager start/stop the PlaybackService as well as relevant notification + */ + void onPlaybackStateChange(PlaybackStateCompat state) { + // Report the state to the MediaSession. + + Log.v(TAG, "onPlaybackStateChange(" + (state != null ? state.getState() : "null") + ")"); + try { + // Manage the started state of this service. + switch (state.getState()) { + case PlaybackStateCompat.STATE_CONNECTING: + // move the service to started, aka, making it foreground + // upon STATE_CONNECTING, i.e., in preparing to play a media. + // This is done so that in case the preparation takes a long time, e.g., + // streaming over a slow network, + // the service won't be killed by the system prematurely. + moveServiceToStartedState(state); + break; + case PlaybackStateCompat.STATE_PLAYING: + moveServiceToStartedState(state); + break; + case PlaybackStateCompat.STATE_PAUSED: + updateNotificationForPause(state); + break; + case PlaybackStateCompat.STATE_STOPPED: + moveServiceOutOfStartedState(state); + break; + case PlaybackStateCompat.STATE_ERROR: + moveServiceOutOfStartedState(state); + break; + } + } finally { + if (toTreatNextPauseAsStopOnUI) { + Log.v(TAG, "onPlaybackStateChange() - toTreatNextPauseAsStopOnUI enabled. The actual state (should be PAUSED, aka 2): " + state.getState()); + toTreatNextPauseAsStopOnUI = false; + } + } + } + + /** + * Tell service manager that on the next state change, if the state is STATE_PAUSED, + * give UI treatment as if it is stopped. + * + * @see #handleKeycode(int, boolean) the use case + */ + public void treatNextPauseAsStopOnUI() { + this.toTreatNextPauseAsStopOnUI = true; + } + + public void showUIForStopState() { + Log.v(TAG, "serviceManager.showUIForStopState()"); + stopForeground(true); // gets rid of persistent notification, to give the UI illusion of STOP + } + + public void stopService() { + stopForeground(true); + stopSelf(); + serviceInStartedState = false; + } + + private void moveServiceToStartedState(PlaybackStateCompat state) { + if (!serviceInStartedState) { + ContextCompat.startForegroundService( + PlaybackService.this, + new Intent(PlaybackService.this, PlaybackService.class)); + serviceInStartedState = true; + } + + doSetupNotification(); + } + + private void updateNotificationForPause(PlaybackStateCompat state) { + doSetupNotification(); + } + + private void moveServiceOutOfStartedState(PlaybackStateCompat state) { + stopService(); + } + + private void doSetupNotification() { + if (mediaPlayer != null && mediaPlayer.getPlayable() != null) { + // it updates notification and set foreground status + // based on player status (similar to PlaybackState) + setupNotification(mediaPlayer.getPlayable(), toTreatNextPauseAsStopOnUI); + } else { + // should not happen unless there are bugs. + Log.e(TAG, "doSetupNotification() - unexpectedly there is no playable. No notification setup done. mediaPlayer." + mediaPlayer); + } + } + } + + private final ServiceManager serviceManager = new ServiceManager(); + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index f1410b894..1daa43025 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -1253,9 +1253,15 @@ public class PodDBAdapter { } public final int getNumberOfNewItems() { - final String query = "SELECT COUNT(" + KEY_ID + ")" - + " FROM " + TABLE_NAME_FEED_ITEMS - + " WHERE " + KEY_READ + "=" + FeedItem.NEW; + Object[] args = new String[]{ + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS, + TABLE_NAME_FEEDS, + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW + + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0" + }; + final String query = String.format("SELECT COUNT(%s) FROM %s INNER JOIN %s ON %s WHERE %s", args); Cursor c = db.rawQuery(query, null); int result = 0; if (c.moveToFirst()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java index b34ba196d..9bdd375ce 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -90,9 +90,13 @@ public class NetworkUtils { return info != null && info.isConnected(); } - public static boolean isDownloadAllowed() { - return UserPreferences.isAllowMobileUpdate() || !NetworkUtils.isNetworkMetered(); - } + public static boolean isDownloadAllowed() { + return UserPreferences.isAllowMobileUpdate() || !NetworkUtils.isNetworkMetered(); + } + + public static boolean isImageAllowed() { + return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkMetered(); + } private static boolean isNetworkMetered() { ConnectivityManager connManager = (ConnectivityManager) context diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java new file mode 100644 index 000000000..0fe11ec53 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package de.danoeh.antennapod.core.util; + +import java.util.NoSuchElementException; +import java.util.Objects; + +// AntennaPod's stripped-down version of Java/Android platform's java.util.Optional +// so that it can be used on lower API level (API level 14) + +// Android-changed: removed ValueBased paragraph. +/** + * A container object which may or may not contain a non-null value. + * If a value is present, {@code isPresent()} will return {@code true} and + * {@code get()} will return the value. + * + * <p>Additional methods that depend on the presence or absence of a contained + * value are provided, such as {@link #orElse(java.lang.Object) orElse()} + * (return a default value if value not present) and + * {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block + * of code if the value is present). + * + * @since 1.8 + */ +public final class Optional<T> { + /** + * Common instance for {@code empty()}. + */ + private static final Optional<?> EMPTY = new Optional<>(); + + /** + * If non-null, the value; if null, indicates no value is present + */ + private final T value; + + /** + * Constructs an empty instance. + * + * @implNote Generally only one empty instance, {@link Optional#EMPTY}, + * should exist per VM. + */ + private Optional() { + this.value = null; + } + + /** + * Returns an empty {@code Optional} instance. No value is present for this + * Optional. + * + * @apiNote Though it may be tempting to do so, avoid testing if an object + * is empty by comparing with {@code ==} against instances returned by + * {@code Option.empty()}. There is no guarantee that it is a singleton. + * Instead, use {@link #isPresent()}. + * + * @param <T> Type of the non-existent value + * @return an empty {@code Optional} + */ + public static<T> Optional<T> empty() { + @SuppressWarnings("unchecked") + Optional<T> t = (Optional<T>) EMPTY; + return t; + } + + /** + * Constructs an instance with the value present. + * + * @param value the non-null value to be present + * @throws NullPointerException if value is null + */ + private Optional(T value) { + this.value = Objects.requireNonNull(value); + } + + /** + * Returns an {@code Optional} with the specified present non-null value. + * + * @param <T> the class of the value + * @param value the value to be present, which must be non-null + * @return an {@code Optional} with the value present + * @throws NullPointerException if value is null + */ + public static <T> Optional<T> of(T value) { + return new Optional<>(value); + } + + /** + * Returns an {@code Optional} describing the specified value, if non-null, + * otherwise returns an empty {@code Optional}. + * + * @param <T> the class of the value + * @param value the possibly-null value to describe + * @return an {@code Optional} with a present value if the specified value + * is non-null, otherwise an empty {@code Optional} + */ + public static <T> Optional<T> ofNullable(T value) { + return value == null ? empty() : of(value); + } + + /** + * If a value is present in this {@code Optional}, returns the value, + * otherwise throws {@code NoSuchElementException}. + * + * @return the non-null value held by this {@code Optional} + * @throws NoSuchElementException if there is no value present + * + * @see Optional#isPresent() + */ + public T get() { + if (value == null) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + /** + * Return {@code true} if there is a value present, otherwise {@code false}. + * + * @return {@code true} if there is a value present, otherwise {@code false} + */ + public boolean isPresent() { + return value != null; + } + + + /** + * Return the value if present, otherwise return {@code other}. + * + * @param other the value to be returned if there is no value present, may + * be null + * @return the value, if present, otherwise {@code other} + */ + public T orElse(T other) { + return value != null ? value : other; + } + + /** + * Indicates whether some other object is "equal to" this Optional. The + * other object is considered equal if: + * <ul> + * <li>it is also an {@code Optional} and; + * <li>both instances have no value present or; + * <li>the present values are "equal to" each other via {@code equals()}. + * </ul> + * + * @param obj an object to be tested for equality + * @return {code true} if the other object is "equal to" this object + * otherwise {@code false} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Optional)) { + return false; + } + + Optional<?> other = (Optional<?>) obj; + return (value == other.value) || (value != null && value.equals(other.value)); + } + + /** + * Returns the hash code value of the present value, if any, or 0 (zero) if + * no value is present. + * + * @return hash code value of the present value or 0 if no value is present + */ + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + + /** + * Returns a non-empty string representation of this Optional suitable for + * debugging. The exact presentation format is unspecified and may vary + * between implementations and versions. + * + * @implSpec If a value is present the result must include its string + * representation in the result. Empty and present Optionals must be + * unambiguously differentiable. + * + * @return the string representation of this instance + */ + @Override + public String toString() { + return value != null + ? String.format("Optional[%s]", value) + : "Optional.empty"; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index bc047bda5..6498b9ff1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -12,8 +12,6 @@ import android.media.MediaPlayer; import android.os.Build; import android.os.IBinder; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -37,6 +35,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; import io.reactivex.Maybe; import io.reactivex.MaybeOnSubscribe; @@ -105,6 +104,7 @@ public abstract class PlaybackController { } private synchronized void initServiceRunning() { + Log.v(TAG, "initServiceRunning()"); if (initialized) { return; } @@ -187,22 +187,15 @@ public abstract class PlaybackController { serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(intent -> { + .subscribe(optionalIntent -> { boolean bound = false; - if (!PlaybackService.started) { - if (intent != null) { - Log.d(TAG, "Calling start service"); - ContextCompat.startForegroundService(activity, intent); - bound = activity.bindService(intent, mConnection, 0); - } else { - status = PlayerStatus.STOPPED; - setupGUI(); - handleStatus(); - } + if (optionalIntent.isPresent()) { + Log.d(TAG, "Calling bind service"); + bound = activity.bindService(optionalIntent.get(), mConnection, 0); } else { - Log.d(TAG, "PlaybackService is running, trying to connect without start command."); - bound = activity.bindService(new Intent(activity, PlaybackService.class), - mConnection, 0); + status = PlayerStatus.STOPPED; + setupGUI(); + handleStatus(); } Log.d(TAG, "Result for service binding: " + bound); }, error -> Log.e(TAG, Log.getStackTraceString(error))); @@ -212,24 +205,26 @@ public abstract class PlaybackController { * Returns an intent that starts the PlaybackService and plays the last * played media or null if no last played media could be found. */ - @Nullable private Intent getPlayLastPlayedMediaIntent() { + @NonNull + private Optional<Intent> getPlayLastPlayedMediaIntent() { Log.d(TAG, "Trying to restore last played media"); Playable media = PlayableUtils.createInstanceFromPreferences(activity); if (media == null) { Log.d(TAG, "No last played media found"); - return null; + return Optional.empty(); } + boolean fileExists = media.localFileAvailable(); boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); if (!fileExists && !lastIsStream && media instanceof FeedMedia) { DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); } - return new PlaybackServiceStarter(activity, media) + return Optional.of(new PlaybackServiceStarter(activity, media) .startWhenPrepared(false) .shouldStream(lastIsStream || !fileExists) - .getIntent(); + .getIntent()); } @@ -587,7 +582,8 @@ public abstract class PlaybackController { .startWhenPrepared(true) .streamIfLastWasStream() .start(); - Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); + Log.d(TAG, "Play/Pause button was pressed, but playbackservice was null - " + + "it is likely to have been released by Android system. Restarting it."); return; } switch (status) { @@ -764,6 +760,7 @@ public abstract class PlaybackController { } public void notifyVideoSurfaceAbandoned() { + Log.v(TAG, "notifyVideoSurfaceAbandoned() - hasPlaybackService=" + (playbackService != null)); if (playbackService != null) { playbackService.notifyVideoSurfaceAbandoned(); } @@ -784,6 +781,7 @@ public abstract class PlaybackController { } private void initServiceNotRunning() { + Log.v(TAG, "initServiceNotRunning()"); mediaLoader = Maybe.create((MaybeOnSubscribe<Playable>) emitter -> { Playable media = getMedia(); if (media != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java index f7d2ee409..64cf61457 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java @@ -2,14 +2,15 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.Intent; -import android.media.MediaPlayer; -import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; +import android.util.Log; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; public class PlaybackServiceStarter { + private static final String TAG = "PlaybackServiceStarter"; + private final Context context; private final Playable media; private boolean startWhenPrepared = false; @@ -66,6 +67,10 @@ public class PlaybackServiceStarter { launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream); launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately); + if (media == null) { + Log.e(TAG, "getIntent() - media is unexpectedly null. intent:" + launchIntent); + } + return launchIntent; } diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 157b040e1..6d310e0e5 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -55,6 +55,18 @@ <item>-1</item> </string-array> + <string-array name="mobile_update_entries"> + <item>@string/pref_mobileUpdate_nothing</item> + <item>@string/pref_mobileUpdate_images</item> + <item>@string/pref_mobileUpdate_everything</item> + </string-array> + + <string-array name="mobile_update_values"> + <item>nothing</item> + <item>images</item> + <item>everything</item> + </string-array> + <string-array name="episode_cleanup_entries"> <item>@string/episode_cleanup_queue_removal</item> <item>0</item> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 6000873b5..3d730516e 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -339,10 +339,25 @@ <string name="enable_sonic">Enable Sonic</string> <!-- Empty list labels --> - <string name="no_items_label">There are no items in this list.</string> + <string name="no_items_header_label">No queued episodes</string> + <string name="no_items_label">You can add episodes to the queue by long-pressing or downloading them.</string> <string name="no_feeds_label">You haven\'t subscribed to any podcasts yet.</string> <string name="no_chapters_label">This episode has no chapters.</string> <string name="no_shownotes_label">This episode has no shownotes.</string> + <string name="no_run_downloads_head_label">No downloads running</string> + <string name="no_run_downloads_label">You can download episodes on the podcast details screen.</string> + <string name="no_comp_downloads_head_label">No downloaded episodes</string> + <string name="no_comp_downloads_label">You can download episodes on the podcast details screen.</string> + <string name="no_log_downloads_head_label">No download log</string> + <string name="no_log_downloads_label">Download logs will appear here when available.</string> + <string name="no_history_head_label">No History</string> + <string name="no_history_label">After you listen to an episode, it will appear here.</string> + <string name="no_all_episodes_head_label">No Episodes</string> + <string name="no_all_episodes_label">When you add a podcast, the episodes will be shown here.</string> + <string name="no_new_episodes_head_label">No new episodes</string> + <string name="no_new_episodes_label">When new episodes arrive, they will be shown here.</string> + <string name="no_fav_episodes_head_label">No favorite episodes</string> + <string name="no_fav_episodes_label">You can add episodes to the favorites by long-pressing them.</string> <!-- Preferences --> <string name="storage_pref">Storage</string> @@ -398,6 +413,9 @@ <string name="pref_unpauseOnBluetoothReconnect_title">Bluetooth Reconnect</string> <string name="pref_mobileUpdate_title">Mobile Updates</string> <string name="pref_mobileUpdate_sum">Allow updates over the mobile data connection</string> + <string name="pref_mobileUpdate_nothing">Nothing</string> + <string name="pref_mobileUpdate_images">Images only</string> + <string name="pref_mobileUpdate_everything">Everything</string> <string name="refreshing_label">Refreshing</string> <string name="flattr_settings_label">Flattr settings</string> <string name="pref_flattr_auth_title">Flattr sign-in</string> diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java index 0cca2ffa4..a6b732a4f 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java @@ -151,7 +151,6 @@ public class PlaybackServiceFlavorHelper { // hardware volume buttons control the local device volume mediaRouter.setMediaSessionCompat(null); unregisterWifiBroadcastReceiver(); - callback.setupNotification(false, info); } }; } @@ -181,7 +180,6 @@ public class PlaybackServiceFlavorHelper { // hardware volume buttons control the remote device volume mediaRouter.setMediaSessionCompat(callback.getMediaSession()); registerWifiBroadcastReceiver(); - callback.setupNotification(true, info); } private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer, |