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 @@ + + + + + + + +