diff --git a/app/build.gradle b/app/build.gradle
index a9f6f9c1f..4c506ba2b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -171,6 +171,7 @@ dependencies {
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
implementation 'com.github.mfietz:fyydlin:v0.5.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
+ implementation "com.github.skydoves:balloon:1.1.5"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
index 44435c02e..3e210c822 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
@@ -12,6 +12,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
@@ -50,6 +51,8 @@ public class DownloadActionButton extends ItemActionButton {
return;
}
+ UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD);
+
if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) {
downloadEpisode(context);
} else if (MobileDownloadHelper.userChoseAddToQueue() && !isInQueue) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
index e5bb04391..527ac3ec1 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
@@ -7,7 +7,6 @@ import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import android.view.View;
-import android.widget.ImageButton;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@@ -47,7 +46,7 @@ public abstract class ItemActionButton {
return new PlayActionButton(item);
} else if (isDownloadingMedia) {
return new CancelDownloadActionButton(item);
- } else if (UserPreferences.streamOverDownload() && allowStream) {
+ } else if (UserPreferences.isStreamOverDownload() && allowStream) {
return new StreamActionButton(item);
} else if (MobileDownloadHelper.userAllowedMobileDownloads()
|| !MobileDownloadHelper.userChoseAddToQueue() || isInQueue) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java
index 88e0fc7ed..8a892a621 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/StreamActionButton.java
@@ -9,6 +9,7 @@ import de.danoeh.antennapod.R;
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.preferences.UsageStatistics;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
@@ -38,6 +39,8 @@ public class StreamActionButton extends ItemActionButton {
if (media == null) {
return;
}
+ UsageStatistics.logAction(UsageStatistics.ACTION_STREAM);
+
if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(context, media).show();
return;
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 72dd0e0c7..ee3f2331f 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -11,18 +11,25 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.core.text.TextUtilsCompat;
import androidx.core.util.ObjectsCompat;
+import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.snackbar.Snackbar;
+import com.skydoves.balloon.ArrowOrientation;
+import com.skydoves.balloon.Balloon;
+import com.skydoves.balloon.BalloonAnimation;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.actionbutton.CancelDownloadActionButton;
@@ -43,11 +50,14 @@ import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
+import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.view.ShownotesWebView;
@@ -61,6 +71,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
+import java.util.Locale;
/**
* Displays information about a FeedItem and actions.
@@ -158,11 +169,60 @@ public class ItemFragment extends Fragment {
butAction2Icon = layout.findViewById(R.id.butAction2Icon);
butAction1Text = layout.findViewById(R.id.butAction1Text);
butAction2Text = layout.findViewById(R.id.butAction2Text);
- butAction1.setOnClickListener(v -> actionButton1.onClick(getContext()));
- butAction2.setOnClickListener(v -> actionButton2.onClick(getContext()));
+
+ butAction1.setOnClickListener(v -> {
+ if (actionButton1 instanceof StreamActionButton && !UserPreferences.isStreamOverDownload()
+ && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM)) {
+ showOnDemandConfigBalloon(true);
+ return;
+ }
+ actionButton1.onClick(getContext());
+ });
+ butAction2.setOnClickListener(v -> {
+ if (actionButton2 instanceof DownloadActionButton && UserPreferences.isStreamOverDownload()
+ && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD)) {
+ showOnDemandConfigBalloon(false);
+ return;
+ }
+ actionButton2.onClick(getContext());
+ });
return layout;
}
+ private void showOnDemandConfigBalloon(boolean offerStreaming) {
+ boolean isLocaleRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ Balloon balloon = new Balloon.Builder(getContext())
+ .setArrowOrientation(ArrowOrientation.TOP)
+ .setArrowPosition(0.25f + ((isLocaleRtl ^ offerStreaming) ? 0f : 0.5f))
+ .setWidthRatio(1.0f)
+ .isRtlSupport(true)
+ .setBackgroundColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSecondary))
+ .setBalloonAnimation(BalloonAnimation.OVERSHOOT)
+ .setLayout(R.layout.popup_bubble_view)
+ .setDismissWhenTouchOutside(true)
+ .setLifecycleOwner(this)
+ .build();
+ Button positiveButton = balloon.getContentView().findViewById(R.id.balloon_button_positive);
+ Button negativeButton = balloon.getContentView().findViewById(R.id.balloon_button_negative);
+ TextView message = balloon.getContentView().findViewById(R.id.balloon_message);
+ message.setText(offerStreaming
+ ? R.string.on_demand_config_stream_text : R.string.on_demand_config_download_text);
+ positiveButton.setOnClickListener(v1 -> {
+ UserPreferences.setStreamOverDownload(offerStreaming);
+ // Update all visible lists to reflect new streaming action button
+ EventBus.getDefault().post(new UnreadItemsUpdateEvent());
+ ((MainActivity) getActivity()).showSnackbarAbovePlayer(
+ R.string.on_demand_config_setting_changed, Snackbar.LENGTH_SHORT);
+ balloon.dismiss();
+ });
+ negativeButton.setOnClickListener(v1 -> {
+ UsageStatistics.askAgainLater(UsageStatistics.ACTION_STREAM); // Type does not matter. Both are silenced.
+ balloon.dismiss();
+ });
+ balloon.showAlignBottom(butAction1, 0, (int) (-12 * getResources().getDisplayMetrics().density));
+ }
+
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
index 6b2255b52..741080cf1 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
@@ -11,6 +11,7 @@ import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
@@ -63,6 +64,7 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> {
// Update all visible lists to reflect new streaming action button
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
+ UsageStatistics.askAgainLater(UsageStatistics.ACTION_STREAM);
return true;
});
diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
index d3900353a..367593131 100644
--- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java
@@ -77,7 +77,7 @@ public class PreferenceUpgrader {
}
UserPreferences.setQueueLocked(false);
- prefs.edit().putBoolean(UserPreferences.PREF_STREAM_OVER_DOWNLOAD, false).apply();
+ UserPreferences.setStreamOverDownload(false);
if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) {
final String keyOldPrefEnqueueFront = "prefQueueAddToFront";
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
index eea7d0ace..3c1eda242 100644
--- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -4,6 +4,7 @@ import android.content.Context;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
@@ -42,6 +43,7 @@ public class ClientConfig {
}
PodDBAdapter.init(context);
UserPreferences.init(context);
+ UsageStatistics.init(context);
PlaybackPreferences.init(context);
NetworkUtils.init(context);
AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java
new file mode 100644
index 000000000..a5b00b08c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UsageStatistics.java
@@ -0,0 +1,73 @@
+package de.danoeh.antennapod.core.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import androidx.annotation.NonNull;
+
+import java.util.Calendar;
+
+/**
+ * Collects statistics about the app usage. The statistics are used to allow on-demand configuration:
+ * "Looks like you stream a lot. Do you want to toggle the 'Prefer streaming' setting?".
+ * The data is only stored locally on the device. It is NOT used for analytics/tracking.
+ * A private instance of this class must first be instantiated via
+ * init() or otherwise every public method will throw an Exception
+ * when called.
+ */
+public class UsageStatistics {
+ private UsageStatistics() {
+
+ }
+
+ private static final String PREF_DB_NAME = "UsageStatistics";
+ private static final float MOVING_AVERAGE_WEIGHT = 0.8f;
+ private static final float MOVING_AVERAGE_BIAS_THRESHOLD = 0.1f;
+ private static final long ASK_AGAIN_LATER_DELAY = 1000 * 3600 * 24 * 10; // 10 days
+ private static final String SUFFIX_HIDDEN_UNTIL = "_hiddenUntil";
+ private static SharedPreferences prefs;
+
+ public static final StatsAction ACTION_STREAM = new StatsAction("downloadVsStream", 0);
+ public static final StatsAction ACTION_DOWNLOAD = new StatsAction("downloadVsStream", 1);
+
+ /**
+ * Sets up the UsageStatistics class.
+ *
+ * @throws IllegalArgumentException if context is null
+ */
+ public static void init(@NonNull Context context) {
+ prefs = context.getSharedPreferences(PREF_DB_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static void logAction(StatsAction action) {
+ int numExecutions = prefs.getInt(action.type + action.value, 0);
+ float movingAverage = prefs.getFloat(action.type, 0.5f);
+ prefs.edit()
+ .putInt(action.type + action.value, numExecutions + 1)
+ .putFloat(action.type, MOVING_AVERAGE_WEIGHT * movingAverage
+ + (1 - MOVING_AVERAGE_WEIGHT) * action.value)
+ .apply();
+ }
+
+ public static boolean hasSignificantBiasTo(StatsAction action) {
+ final float movingAverage = prefs.getFloat(action.type, 0.5f);
+ final long askAfter = prefs.getLong(action.type + SUFFIX_HIDDEN_UNTIL, 0);
+ return Math.abs(action.value - movingAverage) < MOVING_AVERAGE_BIAS_THRESHOLD
+ && Calendar.getInstance().getTimeInMillis() > askAfter;
+ }
+
+ public static void askAgainLater(StatsAction action) {
+ prefs.edit().putLong(action.type + SUFFIX_HIDDEN_UNTIL,
+ Calendar.getInstance().getTimeInMillis() + ASK_AGAIN_LATER_DELAY)
+ .apply();
+ }
+
+ public static final class StatsAction {
+ public final String type;
+ public final int value;
+
+ public StatsAction(String type, int value) {
+ this.type = type;
+ this.value = value;
+ }
+ }
+}
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 cb5a405c0..14900ac20 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
@@ -996,10 +996,14 @@ public class UserPreferences {
return prefs.getBoolean(PREF_TIME_RESPECTS_SPEED, false);
}
- public static boolean streamOverDownload() {
+ public static boolean isStreamOverDownload() {
return prefs.getBoolean(PREF_STREAM_OVER_DOWNLOAD, false);
}
+ public static void setStreamOverDownload(boolean stream) {
+ prefs.edit().putBoolean(PREF_STREAM_OVER_DOWNLOAD, stream).apply();
+ }
+
/**
* Returns if the queue is in keep sorted mode.
*
diff --git a/core/src/main/res/layout/popup_bubble_view.xml b/core/src/main/res/layout/popup_bubble_view.xml
new file mode 100644
index 000000000..6b2e16f99
--- /dev/null
+++ b/core/src/main/res/layout/popup_bubble_view.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 2da946b36..b6e46a9e6 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -810,4 +810,9 @@
Widget settings
Create widget
Opacity
+
+
+ Setting updated successfully.
+ Looks like you stream a lot. Do you want episode lists to show stream buttons?
+ Looks like you download a lot. Do you want episode lists to show download buttons?
diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
index ba30c0ef6..60fd5f4ee 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -10,6 +10,7 @@ import com.google.android.gms.security.ProviderInstaller;
import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
@@ -51,6 +52,7 @@ public class ClientConfig {
}
PodDBAdapter.init(context);
UserPreferences.init(context);
+ UsageStatistics.init(context);
PlaybackPreferences.init(context);
NetworkUtils.init(context);
// Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
diff --git a/settings.gradle b/settings.gradle
index 3bd20b840..bdf2d88b9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':sync', ':core'
+include ':app', ':core'