Added on-demand configuration for stream vs download

This commit is contained in:
ByteHamster 2020-05-05 19:03:44 +02:00
parent b233f4dcb7
commit 8be147c603
14 changed files with 201 additions and 7 deletions

View File

@ -171,6 +171,7 @@ dependencies {
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
implementation 'com.github.mfietz:fyydlin:v0.5.0' implementation 'com.github.mfietz:fyydlin:v0.5.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation "com.github.skydoves:balloon:1.1.5"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1' androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'

View File

@ -12,6 +12,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia; 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.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.storage.DownloadRequester;
@ -50,6 +51,8 @@ public class DownloadActionButton extends ItemActionButton {
return; return;
} }
UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD);
if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) { if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) {
downloadEpisode(context); downloadEpisode(context);
} else if (MobileDownloadHelper.userChoseAddToQueue() && !isInQueue) { } else if (MobileDownloadHelper.userChoseAddToQueue() && !isInQueue) {

View File

@ -7,7 +7,6 @@ import androidx.annotation.AttrRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import android.view.View; import android.view.View;
import android.widget.ImageButton;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMedia;
@ -47,7 +46,7 @@ public abstract class ItemActionButton {
return new PlayActionButton(item); return new PlayActionButton(item);
} else if (isDownloadingMedia) { } else if (isDownloadingMedia) {
return new CancelDownloadActionButton(item); return new CancelDownloadActionButton(item);
} else if (UserPreferences.streamOverDownload() && allowStream) { } else if (UserPreferences.isStreamOverDownload() && allowStream) {
return new StreamActionButton(item); return new StreamActionButton(item);
} else if (MobileDownloadHelper.userAllowedMobileDownloads() } else if (MobileDownloadHelper.userAllowedMobileDownloads()
|| !MobileDownloadHelper.userChoseAddToQueue() || isInQueue) { || !MobileDownloadHelper.userChoseAddToQueue() || isInQueue) {

View File

@ -9,6 +9,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType; 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.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
@ -38,6 +39,8 @@ public class StreamActionButton extends ItemActionButton {
if (media == null) { if (media == null) {
return; return;
} }
UsageStatistics.logAction(UsageStatistics.ACTION_STREAM);
if (!NetworkUtils.isStreamingAllowed()) { if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(context, media).show(); new StreamingConfirmationDialog(context, media).show();
return; return;

View File

@ -11,18 +11,25 @@ import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.text.TextUtilsCompat;
import androidx.core.util.ObjectsCompat; import androidx.core.util.ObjectsCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.snackbar.Snackbar; 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.R;
import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.actionbutton.CancelDownloadActionButton; 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.FeedMedia;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings; 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.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils; 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.PlaybackController;
import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.view.ShownotesWebView; import de.danoeh.antennapod.view.ShownotesWebView;
@ -61,6 +71,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* Displays information about a FeedItem and actions. * Displays information about a FeedItem and actions.
@ -158,11 +169,60 @@ public class ItemFragment extends Fragment {
butAction2Icon = layout.findViewById(R.id.butAction2Icon); butAction2Icon = layout.findViewById(R.id.butAction2Icon);
butAction1Text = layout.findViewById(R.id.butAction1Text); butAction1Text = layout.findViewById(R.id.butAction1Text);
butAction2Text = layout.findViewById(R.id.butAction2Text); 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; 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 @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);

View File

@ -11,6 +11,7 @@ import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; 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.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
@ -63,6 +64,7 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> { findPreference(PREF_PLAYBACK_PREFER_STREAMING).setOnPreferenceChangeListener((preference, newValue) -> {
// Update all visible lists to reflect new streaming action button // Update all visible lists to reflect new streaming action button
EventBus.getDefault().post(new UnreadItemsUpdateEvent()); EventBus.getDefault().post(new UnreadItemsUpdateEvent());
UsageStatistics.askAgainLater(UsageStatistics.ACTION_STREAM);
return true; return true;
}); });

View File

@ -77,7 +77,7 @@ public class PreferenceUpgrader {
} }
UserPreferences.setQueueLocked(false); UserPreferences.setQueueLocked(false);
prefs.edit().putBoolean(UserPreferences.PREF_STREAM_OVER_DOWNLOAD, false).apply(); UserPreferences.setStreamOverDownload(false);
if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) { if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) {
final String keyOldPrefEnqueueFront = "prefQueueAddToFront"; final String keyOldPrefEnqueueFront = "prefQueueAddToFront";

View File

@ -4,6 +4,7 @@ import android.content.Context;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; 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.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.storage.PodDBAdapter;
@ -42,6 +43,7 @@ public class ClientConfig {
} }
PodDBAdapter.init(context); PodDBAdapter.init(context);
UserPreferences.init(context); UserPreferences.init(context);
UsageStatistics.init(context);
PlaybackPreferences.init(context); PlaybackPreferences.init(context);
NetworkUtils.init(context); NetworkUtils.init(context);
AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));

View File

@ -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;
}
}
}

View File

@ -996,10 +996,14 @@ public class UserPreferences {
return prefs.getBoolean(PREF_TIME_RESPECTS_SPEED, false); return prefs.getBoolean(PREF_TIME_RESPECTS_SPEED, false);
} }
public static boolean streamOverDownload() { public static boolean isStreamOverDownload() {
return prefs.getBoolean(PREF_STREAM_OVER_DOWNLOAD, false); 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. * Returns if the queue is in keep sorted mode.
* *

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:textColor="?attr/colorOnSecondary"
android:layout_height="wrap_content"
android:lines="3"
android:id="@+id/balloon_message"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="end">
<Button
style="@style/Widget.MaterialComponents.Button.TextButton"
android:textColor="?attr/colorOnSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no"
android:id="@+id/balloon_button_negative"/>
<Button
style="@style/Widget.MaterialComponents.Button.TextButton"
android:textColor="?attr/colorOnSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/yes"
android:id="@+id/balloon_button_positive"/>
</LinearLayout>
</LinearLayout>

View File

@ -810,4 +810,9 @@
<string name="widget_settings">Widget settings</string> <string name="widget_settings">Widget settings</string>
<string name="widget_create_button">Create widget</string> <string name="widget_create_button">Create widget</string>
<string name="widget_opacity">Opacity</string> <string name="widget_opacity">Opacity</string>
<!-- On-Demand configuration -->
<string name="on_demand_config_setting_changed">Setting updated successfully.</string>
<string name="on_demand_config_stream_text">Looks like you stream a lot. Do you want episode lists to show stream buttons?</string>
<string name="on_demand_config_download_text">Looks like you download a lot. Do you want episode lists to show download buttons?</string>
</resources> </resources>

View File

@ -10,6 +10,7 @@ import com.google.android.gms.security.ProviderInstaller;
import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; 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.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.storage.PodDBAdapter;
@ -51,6 +52,7 @@ public class ClientConfig {
} }
PodDBAdapter.init(context); PodDBAdapter.init(context);
UserPreferences.init(context); UserPreferences.init(context);
UsageStatistics.init(context);
PlaybackPreferences.init(context); PlaybackPreferences.init(context);
NetworkUtils.init(context); NetworkUtils.init(context);
// Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary

View File

@ -1 +1 @@
include ':app', ':sync', ':core' include ':app', ':core'