Nicer rating dialog (#7011)

This commit is contained in:
ByteHamster 2024-03-22 18:18:30 +01:00 committed by GitHub
parent 27aa5cba96
commit 0a6b7ed699
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 308 additions and 169 deletions

View File

@ -125,9 +125,6 @@ dependencies {
implementation 'com.github.skydoves:balloon:1.5.3'
implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3'
// Non-free dependencies:
playImplementation "com.google.android.play:review:2.0.1"
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"

View File

@ -25,7 +25,6 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.NavDrawerFragment;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
@ -166,9 +165,6 @@ public class EspressoTestUtils {
.edit()
.putString(UserPreferences.PREF_UPDATE_INTERVAL, "0")
.commit();
RatingDialog.init(InstrumentationRegistry.getInstrumentation().getTargetContext());
RatingDialog.saveRated();
}
public static void setLaunchScreen(String tag) {

View File

@ -1,13 +0,0 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
public class RatingDialog {
public static void init(Context context) {}
public static void check() {}
@VisibleForTesting
public static void saveRated() {}
}

View File

@ -39,7 +39,7 @@ import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter;
import de.danoeh.antennapod.ui.common.ThemeSwitcher;
import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink;
import de.danoeh.antennapod.core.util.download.FeedUpdateManager;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.dialog.rating.RatingDialogManager;
import de.danoeh.antennapod.event.EpisodeDownloadEvent;
import de.danoeh.antennapod.event.FeedUpdateRunningEvent;
import de.danoeh.antennapod.event.MessageEvent;
@ -496,14 +496,13 @@ public class MainActivity extends CastEnabledActivity {
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
RatingDialog.init(this);
new RatingDialogManager(this).showIfNeeded();
}
@Override
protected void onResume() {
super.onResume();
handleNavIntent();
RatingDialog.check();
if (lastTheme != ThemeSwitcher.getNoTitleTheme(this)) {
finish();

View File

@ -0,0 +1,79 @@
package de.danoeh.antennapod.dialog.rating;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.databinding.RatingDialogBinding;
import de.danoeh.antennapod.ui.common.DateFormatter;
import java.util.Date;
public class RatingDialogFragment extends DialogFragment {
private static final String EXTRA_TOTAL_TIME = "totalTime";
private static final String EXTRA_OLDEST_DATE = "oldestDate";
public static RatingDialogFragment newInstance(long totalTime, long oldestDate) {
RatingDialogFragment fragment = new RatingDialogFragment();
Bundle arguments = new Bundle();
arguments.putLong(EXTRA_TOTAL_TIME, totalTime);
arguments.putLong(EXTRA_OLDEST_DATE, oldestDate);
fragment.setArguments(arguments);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new MaterialAlertDialogBuilder(getContext())
.setView(onCreateView(getLayoutInflater(), null, savedInstanceState))
.create();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
RatingDialogBinding viewBinding = RatingDialogBinding.inflate(inflater);
long totalTime = getArguments().getLong(EXTRA_TOTAL_TIME, 0);
long oldestDate = getArguments().getLong(EXTRA_OLDEST_DATE, 0);
viewBinding.headerLabel.setText(HtmlCompat.fromHtml(getString(R.string.rating_tagline,
DateFormatter.formatAbbrev(getContext(), new Date(oldestDate)),
"<br/><b><big><big><big><big><big>", totalTime / 3600L,
"</big></big></big></big></big></b><br/>"), HtmlCompat.FROM_HTML_MODE_LEGACY));
viewBinding.neverAgainButton.setOnClickListener(v -> {
new RatingDialogManager(getActivity()).saveRated();
dismiss();
});
viewBinding.showLaterButton.setOnClickListener(v -> {
new RatingDialogManager(getActivity()).resetStartDate();
dismiss();
});
viewBinding.rateButton.setOnClickListener(v -> {
IntentUtils.openInBrowser(getContext(),
"https://play.google.com/store/apps/details?id=de.danoeh.antennapod");
new RatingDialogManager(getActivity()).saveRated();
});
viewBinding.contibuteButton.setOnClickListener(v -> {
IntentUtils.openInBrowser(getContext(), IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/");
new RatingDialogManager(getActivity()).saveRated();
});
return viewBinding.getRoot();
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
super.onCancel(dialog);
new RatingDialogManager(getActivity()).resetStartDate();
}
}

View File

@ -0,0 +1,94 @@
package de.danoeh.antennapod.dialog.rating;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.TimeUnit;
import android.util.Log;
import androidx.fragment.app.FragmentActivity;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.StatisticsItem;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import kotlin.Pair;
public class RatingDialogManager {
private static final int AFTER_DAYS = 20;
private static final String TAG = "RatingDialog";
private static final String PREFS_NAME = "RatingPrefs";
private static final String KEY_RATED = "KEY_WAS_RATED";
private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE";
private final SharedPreferences preferences;
private final FragmentActivity fragmentActivity;
private Disposable disposable;
public RatingDialogManager(FragmentActivity activity) {
this.fragmentActivity = activity;
preferences = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public void showIfNeeded() {
//noinspection ConstantConditions
if (isRated() || BuildConfig.DEBUG || "free".equals(BuildConfig.FLAVOR)) {
return;
} else if (!enoughTimeSinceInstall()) {
return;
}
if (disposable != null) {
disposable.dispose();
}
disposable = Observable.fromCallable(
() -> {
DBReader.StatisticsResult statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE);
long totalTime = 0;
for (StatisticsItem item : statisticsData.feedTime) {
totalTime += item.timePlayed;
}
return new Pair<>(totalTime, statisticsData.oldestDate);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
long totalTime = result.getFirst();
long oldestDate = result.getSecond();
if (totalTime < TimeUnit.SECONDS.convert(15, TimeUnit.HOURS)) {
return;
} else if (oldestDate > System.currentTimeMillis()
- TimeUnit.MILLISECONDS.convert(AFTER_DAYS, TimeUnit.DAYS)) {
return; // In case the app was opened but nothing was played
}
RatingDialogFragment.newInstance(result.getFirst(), result.getSecond())
.show(fragmentActivity.getSupportFragmentManager(), TAG);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
private boolean isRated() {
return preferences.getBoolean(KEY_RATED, false);
}
public void saveRated() {
preferences.edit().putBoolean(KEY_RATED, true).apply();
}
public void resetStartDate() {
preferences.edit().putLong(KEY_FIRST_START_DATE, System.currentTimeMillis()).apply();
}
private boolean enoughTimeSinceInstall() {
if (preferences.getLong(KEY_FIRST_START_DATE, 0) == 0) {
resetStartDate();
return false;
}
long now = System.currentTimeMillis();
long firstDate = preferences.getLong(KEY_FIRST_START_DATE, now);
long diff = now - firstDate;
long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return diffDays >= AFTER_DAYS;
}
}

View File

@ -17,13 +17,6 @@ import de.danoeh.antennapod.activity.BugReportActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.fragment.preferences.about.AboutFragment;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public class MainPreferencesFragment extends PreferenceFragmentCompat {
@ -113,7 +106,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
}
);
findPreference(PREF_DOCUMENTATION).setOnPreferenceClickListener(preference -> {
IntentUtils.openInBrowser(getContext(), getLocalizedWebsiteLink() + "/documentation/");
IntentUtils.openInBrowser(getContext(),
IntentUtils.getLocalizedWebsiteLink(getContext()) + "/documentation/");
return true;
});
findPreference(PREF_VIEW_FORUM).setOnPreferenceClickListener(preference -> {
@ -121,7 +115,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
return true;
});
findPreference(PREF_CONTRIBUTE).setOnPreferenceClickListener(preference -> {
IntentUtils.openInBrowser(getContext(), getLocalizedWebsiteLink() + "/contribute/");
IntentUtils.openInBrowser(getContext(),
IntentUtils.getLocalizedWebsiteLink(getContext()) + "/contribute/");
return true;
});
findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> {
@ -130,20 +125,6 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
});
}
private String getLocalizedWebsiteLink() {
try (InputStream is = getContext().getAssets().open("website-languages.txt")) {
String[] languages = IOUtils.toString(is, StandardCharsets.UTF_8.name()).split("\n");
String deviceLanguage = Locale.getDefault().getLanguage();
if (ArrayUtils.contains(languages, deviceLanguage) && !"en".equals(deviceLanguage)) {
return "https://antennapod.org/" + deviceLanguage;
} else {
return "https://antennapod.org";
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void setupSearch() {
SearchPreference searchPreference = findPreference("searchPreference");
SearchConfiguration config = searchPreference.getSearchConfiguration();

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipToPadding="false"
android:paddingBottom="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_blue_gradient"
android:orientation="horizontal"
android:paddingVertical="32dp"
android:paddingHorizontal="16dp">
<ImageView
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:importantForAccessibility="no"
android:src="@drawable/logo_monochrome" />
<TextView
android:id="@+id/headerLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:textColor="#fff"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/rating_volunteers_label"
android:layout_marginHorizontal="32dp"
android:layout_marginVertical="16dp"
android:textColor="?colorOnBackground" />
<Button
android:id="@+id/rateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:text="@string/rating_rate"
android:layout_gravity="right|end"
style="@style/Widget.Material3.Button.OutlinedButton" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/rating_contribute_label"
android:layout_marginHorizontal="32dp"
android:layout_marginVertical="16dp"
android:textColor="?colorOnBackground" />
<Button
android:id="@+id/contibuteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:text="@string/rating_contribute_button"
android:layout_gravity="right|end"
style="@style/Widget.Material3.Button.OutlinedButton" />
<Button
android:id="@+id/showLaterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="8dp"
android:text="@string/rating_later"
android:layout_gravity="right|end"
android:gravity="right|end"
style="@style/Widget.Material3.Button.TextButton" />
<Button
android:id="@+id/neverAgainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:text="@string/checkbox_do_not_show_again"
android:layout_gravity="right|end"
android:gravity="right|end"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout>
</ScrollView>

View File

@ -1,123 +0,0 @@
package de.danoeh.antennapod.dialog;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.VisibleForTesting;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
import com.google.android.gms.tasks.Task;
import com.google.android.play.core.review.ReviewInfo;
import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory;
import de.danoeh.antennapod.BuildConfig;
public class RatingDialog {
private RatingDialog() {
}
private static final String TAG = RatingDialog.class.getSimpleName();
private static final int AFTER_DAYS = 14;
private static WeakReference<Context> mContext;
private static SharedPreferences mPreferences;
private static final String PREFS_NAME = "RatingPrefs";
private static final String KEY_RATED = "KEY_WAS_RATED";
private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE";
private static final String KEY_NUMBER_OF_REVIEWS = "NUMBER_OF_REVIEW_ATTEMPTS";
public static void init(Context context) {
mContext = new WeakReference<>(context);
mPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, 0);
if (firstDate == 0) {
resetStartDate();
}
}
public static void check() {
if (shouldShow()) {
try {
showInAppReview();
} catch (Exception e) {
Log.e(TAG, Log.getStackTraceString(e));
}
}
}
private static void showInAppReview() {
Context context = mContext.get();
if (context == null) {
return;
}
ReviewManager manager = ReviewManagerFactory.create(context);
Task<ReviewInfo> request = manager.requestReviewFlow();
request.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
ReviewInfo reviewInfo = task.getResult();
Task<Void> flow = manager.launchReviewFlow((Activity) context, reviewInfo);
flow.addOnCompleteListener(task1 -> {
int previousAttempts = mPreferences.getInt(KEY_NUMBER_OF_REVIEWS, 0);
if (previousAttempts >= 3) {
saveRated();
} else {
resetStartDate();
mPreferences
.edit()
.putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1)
.apply();
}
Log.i("ReviewDialog", "Successfully finished in-app review");
})
.addOnFailureListener(error -> {
Log.i("ReviewDialog", "failed in reviewing process");
});
}
})
.addOnFailureListener(error -> {
Log.i("ReviewDialog", "failed to get in-app review request");
});
}
private static boolean rated() {
return mPreferences.getBoolean(KEY_RATED, false);
}
@VisibleForTesting
public static void saveRated() {
mPreferences
.edit()
.putBoolean(KEY_RATED, true)
.apply();
}
private static void resetStartDate() {
mPreferences
.edit()
.putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
.apply();
}
private static boolean shouldShow() {
if (rated() || BuildConfig.DEBUG) {
return false;
}
long now = System.currentTimeMillis();
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, now);
long diff = now - firstDate;
long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return diffDays >= AFTER_DAYS;
}
}

View File

@ -9,8 +9,14 @@ import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import de.danoeh.antennapod.core.R;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
public class IntentUtils {
private static final String TAG = "IntentUtils";
@ -45,4 +51,19 @@ public class IntentUtils {
Log.e(TAG, Log.getStackTraceString(e));
}
}
public static String getLocalizedWebsiteLink(Context context) {
try (InputStream is = context.getAssets().open("website-languages.txt")) {
String[] languages = IOUtils.toString(is, StandardCharsets.UTF_8.name()).split("\n");
String deviceLanguage = Locale.getDefault().getLanguage();
if (ArrayUtils.contains(languages, deviceLanguage) && !"en".equals(deviceLanguage)) {
return "https://antennapod.org/" + deviceLanguage;
} else {
return "https://antennapod.org";
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -672,6 +672,14 @@
<string name="pref_pausePlaybackForFocusLoss_sum">Pause playback instead of lowering volume when another app wants to play sounds</string>
<string name="pref_pausePlaybackForFocusLoss_title">Pause for interruptions</string>
<!-- Rating dialog -->
<string name="rating_tagline">Since %1$s, you played %2$s%3$d%4$s hours of podcasts.</string>
<string name="rating_contribute_label">Want to join? Whether you want to translate, support, design or code, we would be happy to have you!</string>
<string name="rating_contribute_button">Discover ways to contribute</string>
<string name="rating_volunteers_label">AntennaPod is developed by volunteers in our free time. We would be happy if you appreciated our work by leaving a nice rating.</string>
<string name="rating_rate">Rate AntennaPod</string>
<string name="rating_later">Later</string>
<!-- Online feed view -->
<string name="subscribe_label">Subscribe</string>
<string name="subscribing_label">Subscribing&#8230;</string>