Added on-demand configuration for stream vs download
This commit is contained in:
parent
b233f4dcb7
commit
8be147c603
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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>
|
|
@ -810,4 +810,9 @@
|
|||
<string name="widget_settings">Widget settings</string>
|
||||
<string name="widget_create_button">Create widget</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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
include ':app', ':sync', ':core'
|
||||
include ':app', ':core'
|
||||
|
|
Loading…
Reference in New Issue