diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-03-22 18:18:30 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-22 18:18:30 +0100 |
commit | 0a6b7ed69957cea9582af9e0dd861ed13795d6b5 (patch) | |
tree | 317262009e7e439a9d85c643fb1f6e864f1d4507 /app | |
parent | 27aa5cba96299dd14569f8a5f6da60bb88352d60 (diff) | |
download | AntennaPod-0a6b7ed69957cea9582af9e0dd861ed13795d6b5.zip |
Nicer rating dialog (#7011)
Diffstat (limited to 'app')
9 files changed, 279 insertions, 169 deletions
diff --git a/app/build.gradle b/app/build.gradle index d0958cd17..559e36a3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index a7575862b..f0e0a6996 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -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) { diff --git a/app/src/free/java/de/danoeh/antennapod/dialog/RatingDialog.java b/app/src/free/java/de/danoeh/antennapod/dialog/RatingDialog.java deleted file mode 100644 index ec470eee6..000000000 --- a/app/src/free/java/de/danoeh/antennapod/dialog/RatingDialog.java +++ /dev/null @@ -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() {} -}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index d38d4e3ad..3c867b88c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -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(); diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java new file mode 100644 index 000000000..4e9da8b9a --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogFragment.java @@ -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(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java new file mode 100644 index 000000000..ffa67cc2e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/rating/RatingDialogManager.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index 4d7313247..bf30700bf 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -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(); diff --git a/app/src/main/res/layout/rating_dialog.xml b/app/src/main/res/layout/rating_dialog.xml new file mode 100644 index 000000000..c8f3c980b --- /dev/null +++ b/app/src/main/res/layout/rating_dialog.xml @@ -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> diff --git a/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java b/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java deleted file mode 100644 index c38ae2659..000000000 --- a/app/src/play/java/de/danoeh/antennapod/dialog/RatingDialog.java +++ /dev/null @@ -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; - } -} |