summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java77
-rw-r--r--app/src/main/res/layout/home_section_echo.xml91
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Converter.java29
-rw-r--r--core/src/main/res/drawable/bg_blue_gradient.xml10
-rw-r--r--core/src/main/res/drawable/ic_arrow_right_white.xml13
-rw-r--r--core/src/main/res/values/colors.xml5
-rw-r--r--settings.gradle1
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java15
-rw-r--r--ui/echo/README.md3
-rw-r--r--ui/echo/build.gradle27
-rw-r--r--ui/echo/src/main/AndroidManifest.xml19
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java399
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java64
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java96
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java35
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java89
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java37
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java38
-rw-r--r--ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java39
-rw-r--r--ui/echo/src/main/res/drawable-nodpi/echo.pngbin0 -> 10930 bytes
-rw-r--r--ui/echo/src/main/res/drawable-nodpi/logo_monochrome.pngbin0 -> 4451 bytes
-rw-r--r--ui/echo/src/main/res/font/sarabun_regular.ttfbin0 -> 83080 bytes
-rw-r--r--ui/echo/src/main/res/font/sarabun_semi_bold.ttfbin0 -> 82952 bytes
-rw-r--r--ui/echo/src/main/res/layout/echo_activity.xml135
-rw-r--r--ui/echo/src/main/res/values-de/echo-strings.xml46
-rw-r--r--ui/echo/src/main/res/values-es/echo-strings.xml46
-rw-r--r--ui/echo/src/main/res/values-fr/echo-strings.xml48
-rw-r--r--ui/echo/src/main/res/values-it/echo-strings.xml46
-rw-r--r--ui/echo/src/main/res/values/echo-strings.xml51
-rw-r--r--ui/i18n/src/main/res/values/strings.xml6
33 files changed, 1480 insertions, 8 deletions
diff --git a/app/build.gradle b/app/build.gradle
index b26dc2fb0..64bc442a8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -89,6 +89,7 @@ dependencies {
implementation project(':storage:preferences')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
+ implementation project(':ui:echo')
implementation project(':ui:glide')
implementation project(':ui:i18n')
implementation project(':ui:statistics')
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java
index 21b36bc49..fc925aa03 100644
--- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java
@@ -21,12 +21,14 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
+import de.danoeh.antennapod.ui.home.sections.EchoSection;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.List;
import de.danoeh.antennapod.R;
@@ -60,6 +62,7 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
public static final String PREF_NAME = "PrefHomeFragment";
public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString";
public static final String PREF_DISABLE_NOTIFICATION_PERMISSION_NAG = "DisableNotificationPermissionNag";
+ public static final String PREF_HIDE_ECHO = "HideEcho";
private static final String KEY_UP_ARROW = "up_arrow";
private boolean displayUpArrow;
@@ -94,13 +97,19 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
private void populateSectionList() {
viewBinding.homeContainer.removeAllViews();
+ SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
if (!prefs.getBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, false)) {
addSection(new AllowNotificationsSection());
}
}
+ if (Calendar.getInstance().get(Calendar.MONTH) == Calendar.DECEMBER
+ && Calendar.getInstance().get(Calendar.YEAR) == 2023
+ && Calendar.getInstance().get(Calendar.DAY_OF_MONTH) >= 10
+ && prefs.getInt(PREF_HIDE_ECHO, 0) != 2023) {
+ addSection(new EchoSection());
+ }
List<String> hiddenSections = getHiddenSections(getContext());
String[] sectionTags = getResources().getStringArray(R.array.home_section_tags);
diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java
new file mode 100644
index 000000000..7261c6be4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EchoSection.java
@@ -0,0 +1,77 @@
+package de.danoeh.antennapod.ui.home.sections;
+
+import android.content.Context;
+import android.content.Intent;
+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.fragment.app.Fragment;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.databinding.HomeSectionEchoBinding;
+import de.danoeh.antennapod.ui.echo.EchoActivity;
+import de.danoeh.antennapod.ui.home.HomeFragment;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.util.Calendar;
+
+public class EchoSection extends Fragment {
+ private HomeSectionEchoBinding viewBinding;
+ private Disposable disposable;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ viewBinding = HomeSectionEchoBinding.inflate(inflater);
+ viewBinding.titleLabel.setText(getString(R.string.antennapod_echo_year, 2023));
+ viewBinding.echoButton.setOnClickListener(v -> startActivity(new Intent(getContext(), EchoActivity.class)));
+ viewBinding.closeButton.setOnClickListener(v -> {
+ getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE)
+ .edit().putInt(HomeFragment.PREF_HIDE_ECHO, 2023).apply();
+ ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null);
+ });
+ updateVisibility();
+ return viewBinding.getRoot();
+ }
+
+ private long jan1() {
+ Calendar date = Calendar.getInstance();
+ date.set(Calendar.HOUR_OF_DAY, 0);
+ date.set(Calendar.MINUTE, 0);
+ date.set(Calendar.SECOND, 0);
+ date.set(Calendar.MILLISECOND, 0);
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ date.set(Calendar.MONTH, 0);
+ date.set(Calendar.YEAR, 2023);
+ return date.getTimeInMillis();
+ }
+
+ private void updateVisibility() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ disposable = Observable.fromCallable(
+ () -> {
+ DBReader.StatisticsResult statisticsResult = DBReader.getStatistics(false, jan1(), Long.MAX_VALUE);
+ long totalTime = 0;
+ for (StatisticsItem feedTime : statisticsResult.feedTime) {
+ totalTime += feedTime.timePlayed;
+ }
+ return totalTime;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(totalTime -> viewBinding.getRoot()
+ .setVisibility((totalTime >= 3600 * 10) ? View.VISIBLE : View.GONE),
+ Throwable::printStackTrace);
+ }
+}
diff --git a/app/src/main/res/layout/home_section_echo.xml b/app/src/main/res/layout/home_section_echo.xml
new file mode 100644
index 000000000..f5fdaa46f
--- /dev/null
+++ b/app/src/main/res/layout/home_section_echo.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="16dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="18sp"
+ android:layout_marginVertical="8dp"
+ android:accessibilityHeading="true"
+ android:layout_weight="1"
+ android:text="@string/echo_home_header" />
+
+ <ImageView
+ android:id="@+id/closeButton"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:padding="16dp"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/close_label"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_cancel" />
+
+ </LinearLayout>
+
+ <androidx.cardview.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:cardCornerRadius="8dp"
+ app:cardElevation="0dp">
+
+ <LinearLayout
+ android:id="@+id/echoButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/bg_blue_gradient"
+ android:orientation="vertical"
+ android:padding="16dp"
+ android:foreground="?attr/selectableItemBackground">
+
+ <TextView
+ android:id="@+id/titleLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:textColor="#fff"
+ android:text="@string/antennapod_echo_year"
+ android:textFontWeight="500"
+ style="@style/TextAppearance.Material3.TitleLarge" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textColor="#fff"
+ android:layout_weight="1"
+ android:text="@string/echo_home_subtitle"
+ style="@style/TextAppearance.Material3.BodyMedium" />
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="bottom"
+ android:textColor="#fff"
+ android:importantForAccessibility="no"
+ android:src="@drawable/ic_arrow_right_white" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+</LinearLayout>
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
index d83557b0c..084b3a7ad 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -806,6 +806,17 @@ public final class DBReader {
return result;
}
+ public static long getTimeBetweenReleaseAndPlayback(long timeFilterFrom, long timeFilterTo) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getTimeBetweenReleaseAndPlayback(timeFilterFrom, timeFilterTo)) {
+ cursor.moveToFirst();
+ long result = Long.parseLong(cursor.getString(0));
+ adapter.close();
+ return result;
+ }
+ }
+
/**
* Returns data necessary for displaying the navigation drawer. This includes
* the list of subscriptions, the number of items in the queue and the number of unread
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java
index 4014937bd..d9c4a5098 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
+import android.content.res.Resources;
import java.util.Locale;
import de.danoeh.antennapod.core.R;
@@ -82,17 +83,31 @@ public final class Converter {
* Converts milliseconds to a localized string containing hours and minutes.
*/
public static String getDurationStringLocalized(Context context, long duration) {
- int h = (int) (duration / HOURS_MIL);
- int rest = (int) (duration - h * HOURS_MIL);
- int m = rest / MINUTES_MIL;
+ return getDurationStringLocalized(context.getResources(), duration);
+ }
+ public static String getDurationStringLocalized(Resources resources, long duration) {
String result = "";
+ int h = (int) (duration / HOURS_MIL);
+ int d = h / 24;
+ if (d > 0) {
+ String days = resources.getQuantityString(R.plurals.time_days_quantified, d, d);
+ result += days.replace(" ", "\u00A0") + " ";
+ h -= d * 24;
+ }
+ int rest = (int) (duration - (d * 24 + h) * HOURS_MIL);
+ int m = rest / MINUTES_MIL;
if (h > 0) {
- String hours = context.getResources().getQuantityString(R.plurals.time_hours_quantified, h, h);
- result += hours + " ";
+ String hours = resources.getQuantityString(R.plurals.time_hours_quantified, h, h);
+ result += hours.replace(" ", "\u00A0");
+ if (d == 0) {
+ result += " ";
+ }
+ }
+ if (d == 0) {
+ String minutes = resources.getQuantityString(R.plurals.time_minutes_quantified, m, m);
+ result += minutes.replace(" ", "\u00A0");
}
- String minutes = context.getResources().getQuantityString(R.plurals.time_minutes_quantified, m, m);
- result += minutes;
return result;
}
diff --git a/core/src/main/res/drawable/bg_blue_gradient.xml b/core/src/main/res/drawable/bg_blue_gradient.xml
new file mode 100644
index 000000000..8ae045b6d
--- /dev/null
+++ b/core/src/main/res/drawable/bg_blue_gradient.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <gradient
+ android:angle="90"
+ android:endColor="@color/gradient_025"
+ android:startColor="@color/gradient_075"
+ android:type="linear" />
+ <corners
+ android:radius="0dp"/>
+</shape>
diff --git a/core/src/main/res/drawable/ic_arrow_right_white.xml b/core/src/main/res/drawable/ic_arrow_right_white.xml
new file mode 100644
index 000000000..4f33bed08
--- /dev/null
+++ b/core/src/main/res/drawable/ic_arrow_right_white.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#ffffff"
+ android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
+
+</vector>
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index 553da121a..ad835faf4 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -23,4 +23,9 @@
<color name="accent_light">#0078C2</color>
<color name="accent_dark">#3D8BFF</color>
+
+ <color name="gradient_000">#364ff3</color>
+ <color name="gradient_025">#2E6FF6</color>
+ <color name="gradient_075">#1EB0FC</color>
+ <color name="gradient_100">#16d0ff</color>
</resources>
diff --git a/settings.gradle b/settings.gradle
index 3111771e8..1e97f7fe2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -21,6 +21,7 @@ include ':storage:preferences'
include ':ui:app-start-intent'
include ':ui:common'
+include ':ui:echo'
include ':ui:glide'
include ':ui:i18n'
include ':ui:png-icons'
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
index 21f12e223..96d80c209 100644
--- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java
@@ -1237,6 +1237,21 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
+ public final Cursor getTimeBetweenReleaseAndPlayback(long timeFilterFrom, long timeFilterTo) {
+ final String from = " FROM " + TABLE_NAME_FEED_ITEMS
+ + JOIN_FEED_ITEM_AND_MEDIA
+ + " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + ">=" + timeFilterFrom
+ + " AND " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ">=" + timeFilterFrom
+ + " AND " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + "<" + timeFilterTo;
+ final String query = "SELECT " + TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME
+ + " - " + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " AS diff"
+ + from
+ + " ORDER BY diff ASC"
+ + " LIMIT 1"
+ + " OFFSET (SELECT count(*)/2 " + from + ")";
+ return db.rawQuery(query, null);
+ }
+
public int getQueueSize() {
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
Cursor c = db.rawQuery(query, null);
diff --git a/ui/echo/README.md b/ui/echo/README.md
new file mode 100644
index 000000000..93fa4904a
--- /dev/null
+++ b/ui/echo/README.md
@@ -0,0 +1,3 @@
+# :ui:echo
+
+This module provides the "Echo" screen, a yearly rewind.
diff --git a/ui/echo/build.gradle b/ui/echo/build.gradle
new file mode 100644
index 000000000..de949d18b
--- /dev/null
+++ b/ui/echo/build.gradle
@@ -0,0 +1,27 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ namespace "de.danoeh.antennapod.ui.echo"
+
+ lint {
+ disable "AppBundleLocaleChanges"
+ }
+}
+
+dependencies {
+ implementation project(":core")
+ implementation project(":model")
+ implementation project(":storage:preferences")
+ implementation project(':ui:glide')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "com.google.android.material:material:$googleMaterialVersion"
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
+}
diff --git a/ui/echo/src/main/AndroidManifest.xml b/ui/echo/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..86f938428
--- /dev/null
+++ b/ui/echo/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:installLocation="auto">
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:supportsRtl="true">
+ <activity
+ android:label="@string/antennapod_echo"
+ android:name=".EchoActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AntennaPod.Dark.NoTitle">
+ <intent-filter>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java
new file mode 100644
index 000000000..92adaed2e
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoActivity.java
@@ -0,0 +1,399 @@
+package de.danoeh.antennapod.ui.echo;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ShareCompat;
+import androidx.core.content.FileProvider;
+import androidx.core.view.WindowCompat;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.request.RequestOptions;
+import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.StatisticsItem;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.ui.echo.databinding.EchoActivityBinding;
+import de.danoeh.antennapod.ui.echo.screens.BubbleScreen;
+import de.danoeh.antennapod.ui.echo.screens.FinalShareScreen;
+import de.danoeh.antennapod.ui.echo.screens.RotatingSquaresScreen;
+import de.danoeh.antennapod.ui.echo.screens.StripesScreen;
+import de.danoeh.antennapod.ui.echo.screens.WaveformScreen;
+import io.reactivex.Flowable;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+public class EchoActivity extends AppCompatActivity {
+ private static final String TAG = "EchoActivity";
+ private static final int NUM_SCREENS = 7;
+ private static final int SHARE_SIZE = 1000;
+
+ private EchoActivityBinding viewBinding;
+ private int currentScreen = -1;
+ private boolean progressPaused = false;
+ private float progress = 0;
+ private Drawable currentDrawable;
+ private EchoProgress echoProgress;
+ private Disposable redrawTimer;
+ private long timeTouchDown;
+ private long timeLastFrame;
+ private Disposable disposable;
+
+ private long totalTime = 0;
+ private int totalActivePodcasts = 0;
+ private int playedPodcasts = 0;
+ private int playedActivePodcasts = 0;
+ private String randomUnplayedActivePodcast = "";
+ private int queueNumEpisodes = 0;
+ private long queueSecondsLeft = 0;
+ private long timeBetweenReleaseAndPlay = 0;
+ private long oldestDate = 0;
+ private final ArrayList<Pair<String, Drawable>> favoritePods = new ArrayList<>();
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
+ super.onCreate(savedInstanceState);
+ viewBinding = EchoActivityBinding.inflate(getLayoutInflater());
+ viewBinding.closeButton.setOnClickListener(v -> finish());
+ viewBinding.shareButton.setOnClickListener(v -> share());
+ viewBinding.echoImage.setOnTouchListener((v, event) -> {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ progressPaused = true;
+ timeTouchDown = System.currentTimeMillis();
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ progressPaused = false;
+ if (timeTouchDown + 500 > System.currentTimeMillis()) {
+ int newScreen;
+ if (event.getX() < 0.5f * viewBinding.echoImage.getMeasuredWidth()) {
+ newScreen = Math.max(currentScreen - 1, 0);
+ } else {
+ newScreen = Math.min(currentScreen + 1, NUM_SCREENS - 1);
+ if (currentScreen == NUM_SCREENS - 1) {
+ finish();
+ }
+ }
+ progress = newScreen;
+ echoProgress.setProgress(progress);
+ loadScreen(newScreen, false);
+ }
+ }
+ return true;
+ });
+ echoProgress = new EchoProgress(NUM_SCREENS);
+ viewBinding.echoProgressImage.setImageDrawable(echoProgress);
+ setContentView(viewBinding.getRoot());
+ loadScreen(0, false);
+ loadStatistics();
+ }
+
+ private void share() {
+ try {
+ Bitmap bitmap = Bitmap.createBitmap(SHARE_SIZE, SHARE_SIZE, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ currentDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ currentDrawable.draw(canvas);
+ viewBinding.echoImage.setImageDrawable(null);
+ viewBinding.echoImage.setImageDrawable(currentDrawable);
+ File file = new File(UserPreferences.getDataFolder(null), "AntennaPodEcho.png");
+ FileOutputStream stream = new FileOutputStream(file);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
+ stream.close();
+
+ Uri fileUri = FileProvider.getUriForFile(this, getString(R.string.provider_authority), file);
+ new ShareCompat.IntentBuilder(this)
+ .setType("image/png")
+ .addStream(fileUri)
+ .setText(getString(R.string.echo_share, 2023))
+ .setChooserTitle(R.string.share_file_label)
+ .startChooser();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ redrawTimer = Flowable.timer(20, TimeUnit.MILLISECONDS)
+ .observeOn(Schedulers.io())
+ .repeat()
+ .subscribe(i -> {
+ if (progressPaused) {
+ return;
+ }
+ viewBinding.echoImage.postInvalidate();
+ if (progress >= NUM_SCREENS - 0.001f) {
+ return;
+ }
+ long timePassed = System.currentTimeMillis() - timeLastFrame;
+ timeLastFrame = System.currentTimeMillis();
+ if (timePassed > 500) {
+ timePassed = 0;
+ }
+ progress = Math.min(NUM_SCREENS - 0.001f, progress + timePassed / 10000.0f);
+ echoProgress.setProgress(progress);
+ viewBinding.echoProgressImage.postInvalidate();
+ loadScreen((int) progress, false);
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ redrawTimer.dispose();
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ }
+
+ private void loadScreen(int screen, boolean force) {
+ if (screen == currentScreen && !force) {
+ return;
+ }
+ currentScreen = screen;
+ runOnUiThread(() -> {
+ viewBinding.echoLogo.setVisibility(currentScreen == 0 ? View.VISIBLE : View.GONE);
+ viewBinding.shareButton.setVisibility(currentScreen == 6 ? View.VISIBLE : View.GONE);
+
+ switch (currentScreen) {
+ case 0:
+ viewBinding.aboveLabel.setText(R.string.echo_intro_your_year);
+ viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", 2023));
+ viewBinding.belowLabel.setText(R.string.echo_intro_in_podcasts);
+ viewBinding.smallLabel.setText(R.string.echo_intro_locally);
+ currentDrawable = new BubbleScreen(this);
+ break;
+ case 1:
+ viewBinding.aboveLabel.setText(R.string.echo_hours_this_year);
+ viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", totalTime / 3600));
+ viewBinding.belowLabel.setText(getResources()
+ .getQuantityString(R.plurals.echo_hours_podcasts, playedPodcasts, playedPodcasts));
+ viewBinding.smallLabel.setText("");
+ currentDrawable = new WaveformScreen(this);
+ break;
+ case 2:
+ viewBinding.largeLabel.setText(String.format(getEchoLanguage(), "%d", queueSecondsLeft / 3600));
+ viewBinding.belowLabel.setText(getResources().getQuantityString(
+ R.plurals.echo_queue_hours_waiting, queueNumEpisodes, queueNumEpisodes));
+ int daysUntil2024 = Math.max(356 - Calendar.getInstance().get(Calendar.DAY_OF_YEAR) + 1, 1);
+ long secondsPerDay = queueSecondsLeft / daysUntil2024;
+ String timePerDay = Converter.getDurationStringLocalized(
+ getLocalizedResources(this, getEchoLanguage()), secondsPerDay * 1000);
+ double hoursPerDay = (double) (secondsPerDay / 3600);
+ if (hoursPerDay < 1.5) {
+ viewBinding.aboveLabel.setText(R.string.echo_queue_title_clean);
+ viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_clean, timePerDay, 2024));
+ } else if (hoursPerDay <= 24) {
+ viewBinding.aboveLabel.setText(R.string.echo_queue_title_many);
+ viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_normal, timePerDay, 2024));
+ } else {
+ viewBinding.aboveLabel.setText(R.string.echo_queue_title_many);
+ viewBinding.smallLabel.setText(getString(R.string.echo_queue_hours_much, timePerDay, 2024));
+ }
+ currentDrawable = new StripesScreen(this);
+ break;
+ case 3:
+ viewBinding.aboveLabel.setText(R.string.echo_listened_after_title);
+ if (timeBetweenReleaseAndPlay <= 1000L * 3600 * 24 * 2.5) {
+ viewBinding.largeLabel.setText(R.string.echo_listened_after_emoji_run);
+ viewBinding.belowLabel.setText(R.string.echo_listened_after_comment_addict);
+ } else {
+ viewBinding.largeLabel.setText(R.string.echo_listened_after_emoji_yoga);
+ viewBinding.belowLabel.setText(R.string.echo_listened_after_comment_easy);
+ }
+ viewBinding.smallLabel.setText(getString(R.string.echo_listened_after_time,
+ Converter.getDurationStringLocalized(
+ getLocalizedResources(this, getEchoLanguage()), timeBetweenReleaseAndPlay)));
+ currentDrawable = new RotatingSquaresScreen(this);
+ break;
+ case 4:
+ viewBinding.aboveLabel.setText(R.string.echo_hoarder_title);
+ int percentagePlayed = (int) (100.0 * playedActivePodcasts / totalActivePodcasts);
+ if (percentagePlayed < 25) {
+ viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_cabinet);
+ viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_hoarder);
+ viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_hoarder,
+ percentagePlayed, totalActivePodcasts));
+ } else if (percentagePlayed < 75) {
+ viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_check);
+ viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_medium);
+ viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_medium,
+ percentagePlayed, totalActivePodcasts, randomUnplayedActivePodcast));
+ } else {
+ viewBinding.largeLabel.setText(R.string.echo_hoarder_emoji_clean);
+ viewBinding.belowLabel.setText(R.string.echo_hoarder_subtitle_clean);
+ viewBinding.smallLabel.setText(getString(R.string.echo_hoarder_comment_clean,
+ percentagePlayed, totalActivePodcasts));
+ }
+ currentDrawable = new StripesScreen(this);
+ break;
+ case 5:
+ viewBinding.aboveLabel.setText("");
+ viewBinding.largeLabel.setText(R.string.echo_thanks_large);
+ if (oldestDate < jan1()) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM yyyy", getEchoLanguage());
+ String dateFrom = dateFormat.format(new Date(oldestDate));
+ viewBinding.belowLabel.setText(getString(R.string.echo_thanks_we_are_glad_old, dateFrom));
+ } else {
+ viewBinding.belowLabel.setText(R.string.echo_thanks_we_are_glad_new);
+ }
+ viewBinding.smallLabel.setText(R.string.echo_thanks_now_favorite);
+ currentDrawable = new RotatingSquaresScreen(this);
+ break;
+ case 6:
+ viewBinding.aboveLabel.setText("");
+ viewBinding.largeLabel.setText("");
+ viewBinding.belowLabel.setText("");
+ viewBinding.smallLabel.setText("");
+ currentDrawable = new FinalShareScreen(this, favoritePods);
+ break;
+ default: // Keep
+ }
+ viewBinding.echoImage.setImageDrawable(currentDrawable);
+ });
+ }
+
+ private Locale getEchoLanguage() {
+ boolean hasTranslation = !getString(R.string.echo_listened_after_title)
+ .equals(getLocalizedResources(this, Locale.US).getString(R.string.echo_listened_after_title));
+ if (hasTranslation) {
+ return Locale.getDefault();
+ } else {
+ return Locale.US;
+ }
+ }
+
+ @NonNull
+ private Resources getLocalizedResources(Context context, Locale desiredLocale) {
+ Configuration conf = context.getResources().getConfiguration();
+ conf = new Configuration(conf);
+ conf.setLocale(desiredLocale);
+ Context localizedContext = context.createConfigurationContext(conf);
+ return localizedContext.getResources();
+ }
+
+ private long jan1() {
+ Calendar date = Calendar.getInstance();
+ date.set(Calendar.HOUR_OF_DAY, 0);
+ date.set(Calendar.MINUTE, 0);
+ date.set(Calendar.SECOND, 0);
+ date.set(Calendar.MILLISECOND, 0);
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ date.set(Calendar.MONTH, 0);
+ date.set(Calendar.YEAR, 2023);
+ return date.getTimeInMillis();
+ }
+
+ private void loadStatistics() {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ long timeFilterFrom = jan1();
+ long timeFilterTo = Long.MAX_VALUE;
+ disposable = Observable.fromCallable(
+ () -> {
+ DBReader.StatisticsResult statisticsData = DBReader.getStatistics(
+ false, timeFilterFrom, timeFilterTo);
+ Collections.sort(statisticsData.feedTime, (item1, item2) ->
+ Long.compare(item2.timePlayed, item1.timePlayed));
+
+ favoritePods.clear();
+ for (int i = 0; i < 5 && i < statisticsData.feedTime.size(); i++) {
+ BitmapDrawable cover = new BitmapDrawable(getResources(), (Bitmap) null);
+ try {
+ final int size = SHARE_SIZE / 3;
+ final int radius = (i == 0) ? (size / 16) : (size / 8);
+ cover = new BitmapDrawable(getResources(), Glide.with(this)
+ .asBitmap()
+ .load(statisticsData.feedTime.get(i).feed.getImageUrl())
+ .apply(new RequestOptions()
+ .fitCenter()
+ .transform(new RoundedCorners(radius)))
+ .submit(size, size)
+ .get(1, TimeUnit.SECONDS));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ favoritePods.add(new Pair<>(statisticsData.feedTime.get(i).feed.getTitle(), cover));
+ }
+
+ totalActivePodcasts = 0;
+ playedActivePodcasts = 0;
+ playedPodcasts = 0;
+ totalTime = 0;
+ ArrayList<String> unplayedActive = new ArrayList<>();
+ for (StatisticsItem item : statisticsData.feedTime) {
+ totalTime += item.timePlayed;
+ if (item.timePlayed > 0) {
+ playedPodcasts++;
+ }
+ if (item.feed.getPreferences().getKeepUpdated()) {
+ totalActivePodcasts++;
+ if (item.timePlayed > 0) {
+ playedActivePodcasts++;
+ } else {
+ unplayedActive.add(item.feed.getTitle());
+ }
+ }
+ }
+ if (!unplayedActive.isEmpty()) {
+ randomUnplayedActivePodcast = unplayedActive.get((int) (Math.random() * unplayedActive.size()));
+ }
+
+ List<FeedItem> queue = DBReader.getQueue();
+ queueNumEpisodes = queue.size();
+ queueSecondsLeft = 0;
+ for (FeedItem item : queue) {
+ float playbackSpeed = 1;
+ if (UserPreferences.timeRespectsSpeed()) {
+ playbackSpeed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(item.getMedia());
+ }
+ if (item.getMedia() != null) {
+ long itemTimeLeft = item.getMedia().getDuration() - item.getMedia().getPosition();
+ queueSecondsLeft += itemTimeLeft / playbackSpeed;
+ }
+ }
+ queueSecondsLeft /= 1000;
+
+ timeBetweenReleaseAndPlay = DBReader.getTimeBetweenReleaseAndPlayback(timeFilterFrom, timeFilterTo);
+ oldestDate = statisticsData.oldestDate;
+ return statisticsData;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> loadScreen(currentScreen, true),
+ error -> Log.e(TAG, Log.getStackTraceString(error)));
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java
new file mode 100644
index 000000000..e089bfa09
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java
@@ -0,0 +1,64 @@
+package de.danoeh.antennapod.ui.echo;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+public class EchoProgress extends Drawable {
+ private final Paint paint;
+ private final int numScreens;
+ private float progress = 0;
+
+ public EchoProgress(int numScreens) {
+ this.numScreens = numScreens;
+ paint = new Paint();
+ paint.setFlags(Paint.ANTI_ALIAS_FLAG);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeJoin(Paint.Join.ROUND);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+ paint.setColor(0xffffffff);
+ }
+
+ public void setProgress(float progress) {
+ this.progress = progress;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ paint.setStrokeWidth(0.5f * getBounds().height());
+
+ float y = 0.5f * getBounds().height();
+ float sectionWidth = 1.0f * getBounds().width() / numScreens;
+ float sectionPadding = 0.03f * sectionWidth;
+
+ for (int i = 0; i < numScreens; i++) {
+ if (i + 1 < progress) {
+ paint.setAlpha(255);
+ } else {
+ paint.setAlpha(100);
+ }
+ canvas.drawLine(i * sectionWidth + sectionPadding, y, (i + 1) * sectionWidth - sectionPadding, y, paint);
+ if (Math.floor(1.0 * i) == Math.floor(progress)) {
+ paint.setAlpha(255);
+ canvas.drawLine(i * sectionWidth + sectionPadding, y, i * sectionWidth + sectionPadding
+ + (progress - i) * (sectionWidth - 2 * sectionPadding), y, paint);
+ }
+ }
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java
new file mode 100644
index 000000000..e8bc085cd
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BaseScreen.java
@@ -0,0 +1,96 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import java.util.ArrayList;
+import de.danoeh.antennapod.ui.echo.R;
+
+public abstract class BaseScreen extends Drawable {
+ private final Paint paintBackground;
+ protected final Paint paintParticles;
+ protected final ArrayList<Particle> particles = new ArrayList<>();
+ private final int colorBackgroundFrom;
+ private final int colorBackgroundTo;
+ private long lastFrame = 0;
+
+ public BaseScreen(Context context) {
+ colorBackgroundFrom = ContextCompat.getColor(context, R.color.gradient_000);
+ colorBackgroundTo = ContextCompat.getColor(context, R.color.gradient_100);
+ paintBackground = new Paint();
+ paintParticles = new Paint();
+ paintParticles.setColor(0xffffffff);
+ paintParticles.setFlags(Paint.ANTI_ALIAS_FLAG);
+ paintParticles.setStyle(Paint.Style.FILL);
+ paintParticles.setAlpha(25);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ float width = getBounds().width();
+ float height = getBounds().height();
+ paintBackground.setShader(new LinearGradient(0, 0, 0, height,
+ colorBackgroundFrom, colorBackgroundTo, Shader.TileMode.CLAMP));
+ canvas.drawRect(0, 0, width, height, paintBackground);
+
+ long timeSinceLastFrame = System.currentTimeMillis() - lastFrame;
+ lastFrame = System.currentTimeMillis();
+ if (timeSinceLastFrame > 500) {
+ timeSinceLastFrame = 0;
+ }
+ final float innerBoxSize = (Math.abs(width - height) < 0.001f) // Square share version
+ ? (0.9f * width) : (0.9f * Math.min(width, 0.7f * height));
+ final float innerBoxX = (width - innerBoxSize) / 2;
+ final float innerBoxY = (height - innerBoxSize) / 2;
+
+ for (Particle p : particles) {
+ drawParticle(canvas, p, width, height, innerBoxX, innerBoxY, innerBoxSize);
+ particleTick(p, timeSinceLastFrame);
+ }
+
+ drawInner(canvas, innerBoxX, innerBoxY, innerBoxSize);
+ }
+
+ protected void drawInner(Canvas canvas, float innerBoxX, float innerBoxY, float innerBoxSize) {
+ }
+
+ protected abstract void particleTick(Particle p, long timeSinceLastFrame);
+
+ protected abstract void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
+ float innerBoxX, float innerBoxY, float innerBoxSize);
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ }
+
+ protected static class Particle {
+ double positionX;
+ double positionY;
+ double positionZ;
+ double speed;
+
+ public Particle(double positionX, double positionY, double positionZ, double speed) {
+ this.positionX = positionX;
+ this.positionY = positionY;
+ this.positionZ = positionZ;
+ this.speed = speed;
+ }
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java
new file mode 100644
index 000000000..bd79645dc
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/BubbleScreen.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import androidx.annotation.NonNull;
+
+public class BubbleScreen extends BaseScreen {
+ protected static final double PARTICLE_SPEED = 0.00002;
+ protected static final int NUM_PARTICLES = 15;
+
+ public BubbleScreen(Context context) {
+ super(context);
+ for (int i = 0; i < NUM_PARTICLES; i++) {
+ particles.add(new Particle(Math.random(), 2.0 * Math.random() - 0.5, // Could already be off-screen
+ 0, PARTICLE_SPEED + 2 * PARTICLE_SPEED * Math.random()));
+ }
+ }
+
+ @Override
+ protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
+ float innerBoxX, float innerBoxY, float innerBoxSize) {
+ canvas.drawCircle((float) (width * p.positionX), (float) (p.positionY * height),
+ innerBoxSize / 5, paintParticles);
+ }
+
+ @Override
+ protected void particleTick(Particle p, long timeSinceLastFrame) {
+ p.positionY -= p.speed * timeSinceLastFrame;
+ if (p.positionY < -0.5) {
+ p.positionX = Math.random();
+ p.positionY = 1.5f;
+ p.speed = PARTICLE_SPEED + 2 * PARTICLE_SPEED * Math.random();
+ }
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java
new file mode 100644
index 000000000..87b9cbd53
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/FinalShareScreen.java
@@ -0,0 +1,89 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.util.Pair;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.content.res.ResourcesCompat;
+import de.danoeh.antennapod.ui.echo.R;
+import java.util.ArrayList;
+
+public class FinalShareScreen extends BubbleScreen {
+ private static final float[][] COVER_POSITIONS = new float[][]{ new float[] {0.0f, 0.0f},
+ new float[] {0.4f, 0.0f}, new float[] {0.4f, 0.2f}, new float[] {0.6f, 0.2f}, new float[] {0.8f, 0.2f}};
+ private final Paint paintTextMain;
+ private final Paint paintCoverBorder;
+ private final String heading;
+ private final Drawable logo;
+ private final ArrayList<Pair<String, Drawable>> favoritePods;
+ private final Typeface typefaceNormal;
+ private final Typeface typefaceBold;
+
+ public FinalShareScreen(Context context, ArrayList<Pair<String, Drawable>> favoritePods) {
+ super(context);
+ this.heading = context.getString(R.string.echo_share_heading);
+ this.logo = AppCompatResources.getDrawable(context, R.drawable.echo);
+ this.favoritePods = favoritePods;
+ typefaceNormal = ResourcesCompat.getFont(context, R.font.sarabun_regular);
+ typefaceBold = ResourcesCompat.getFont(context, R.font.sarabun_semi_bold);
+ paintTextMain = new Paint();
+ paintTextMain.setColor(0xffffffff);
+ paintTextMain.setFlags(Paint.ANTI_ALIAS_FLAG);
+ paintTextMain.setStyle(Paint.Style.FILL);
+ paintCoverBorder = new Paint();
+ paintCoverBorder.setColor(0xffffffff);
+ paintCoverBorder.setFlags(Paint.ANTI_ALIAS_FLAG);
+ paintCoverBorder.setStyle(Paint.Style.FILL);
+ paintCoverBorder.setAlpha(70);
+ }
+
+ protected void drawInner(Canvas canvas, float innerBoxX, float innerBoxY, float innerBoxSize) {
+ paintTextMain.setTextAlign(Paint.Align.CENTER);
+ paintTextMain.setTypeface(typefaceBold);
+ float headingSize = innerBoxSize / 14;
+ paintTextMain.setTextSize(headingSize);
+ canvas.drawText(heading, innerBoxX + 0.5f * innerBoxSize, innerBoxY + headingSize, paintTextMain);
+ paintTextMain.setTextSize(0.12f * innerBoxSize);
+ canvas.drawText("2023", innerBoxX + 0.8f * innerBoxSize, innerBoxY + 0.25f * innerBoxSize, paintTextMain);
+
+ paintTextMain.setTextAlign(Paint.Align.LEFT);
+ float fontSizePods = innerBoxSize / 18; // First one only
+ float textY = innerBoxY + 0.62f * innerBoxSize;
+ for (int i = 0; i < favoritePods.size(); i++) {
+ float coverSize = (i == 0) ? (0.4f * innerBoxSize) : (0.2f * innerBoxSize);
+ float coverX = COVER_POSITIONS[i][0];
+ float coverY = COVER_POSITIONS[i][1];
+ RectF logo1Pos = new RectF(innerBoxX + coverX * innerBoxSize,
+ innerBoxY + (coverY + 0.12f) * innerBoxSize,
+ innerBoxX + coverX * innerBoxSize + coverSize,
+ innerBoxY + (coverY + 0.12f) * innerBoxSize + coverSize);
+ logo1Pos.inset((int) (0.01f * innerBoxSize), (int) (0.01f * innerBoxSize));
+ float radius = (i == 0) ? (coverSize / 16) : (coverSize / 8);
+ canvas.drawRoundRect(logo1Pos, radius, radius, paintCoverBorder);
+ logo1Pos.inset((int) (0.003f * innerBoxSize), (int) (0.003f * innerBoxSize));
+ Rect pos = new Rect();
+ logo1Pos.round(pos);
+ favoritePods.get(i).second.setBounds(pos);
+ favoritePods.get(i).second.draw(canvas);
+
+ paintTextMain.setTextSize(fontSizePods);
+ canvas.drawText((i + 1) + ".", innerBoxX, textY, paintTextMain);
+ canvas.drawText(favoritePods.get(i).first, innerBoxX + 0.055f * innerBoxSize, textY, paintTextMain);
+ fontSizePods = innerBoxSize / 24; // Starting with second text is smaller
+ textY += 1.3f * fontSizePods;
+ paintTextMain.setTypeface(typefaceNormal);
+ }
+
+ double ratio = (1.0 * logo.getIntrinsicHeight()) / logo.getIntrinsicWidth();
+ logo.setBounds((int) (innerBoxX + 0.1 * innerBoxSize),
+ (int) (innerBoxY + innerBoxSize - 0.8 * innerBoxSize * ratio),
+ (int) (innerBoxX + 0.9 * innerBoxSize),
+ (int) (innerBoxY + innerBoxSize));
+ logo.draw(canvas);
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java
new file mode 100644
index 000000000..624b68f88
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/RotatingSquaresScreen.java
@@ -0,0 +1,37 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import androidx.annotation.NonNull;
+
+public class RotatingSquaresScreen extends BaseScreen {
+ public RotatingSquaresScreen(Context context) {
+ super(context);
+ for (int i = 0; i < 16; i++) {
+ particles.add(new Particle(
+ 0.3 * (float) (i % 4) + 0.05 + 0.1 * Math.random() - 0.05,
+ 0.2 * (float) (i / 4) + 0.20 + 0.1 * Math.random() - 0.05,
+ Math.random(), 0.00001 * (2 * Math.random() + 2)));
+ }
+ }
+
+ @Override
+ protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
+ float innerBoxX, float innerBoxY, float innerBoxSize) {
+ float x = (float) (p.positionX * width);
+ float y = (float) (p.positionY * height);
+ float size = innerBoxSize / 6;
+ canvas.save();
+ canvas.rotate((float) (360 * p.positionZ), x, y);
+ canvas.drawRect(x - size, y - size, x + size, y + size, paintParticles);
+ canvas.restore();
+ }
+
+ @Override
+ protected void particleTick(Particle p, long timeSinceLastFrame) {
+ p.positionZ += p.speed * timeSinceLastFrame;
+ if (p.positionZ > 1) {
+ p.positionZ -= 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java
new file mode 100644
index 000000000..60906776f
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/StripesScreen.java
@@ -0,0 +1,38 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import androidx.annotation.NonNull;
+
+public class StripesScreen extends BaseScreen {
+ protected static final int NUM_PARTICLES = 15;
+
+ public StripesScreen(Context context) {
+ super(context);
+ for (int i = 0; i < NUM_PARTICLES; i++) {
+ particles.add(new Particle(2f * i / NUM_PARTICLES - 1f, 0, 0, 0));
+ }
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ paintParticles.setStrokeWidth(0.05f * getBounds().width());
+ super.draw(canvas);
+ }
+
+ @Override
+ protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
+ float innerBoxX, float innerBoxY, float innerBoxSize) {
+ float strokeWidth = 0.05f * width;
+ float x = (float) (width * p.positionX);
+ canvas.drawLine(x, -strokeWidth, x + width, height + strokeWidth, paintParticles);
+ }
+
+ @Override
+ protected void particleTick(Particle p, long timeSinceLastFrame) {
+ p.positionX += 0.00005 * timeSinceLastFrame;
+ if (p.positionX > 1f) {
+ p.positionX -= 2f;
+ }
+ }
+}
diff --git a/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java
new file mode 100644
index 000000000..d87f7fbb5
--- /dev/null
+++ b/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/screens/WaveformScreen.java
@@ -0,0 +1,39 @@
+package de.danoeh.antennapod.ui.echo.screens;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import androidx.annotation.NonNull;
+
+public class WaveformScreen extends BaseScreen {
+ protected static final int NUM_PARTICLES = 40;
+
+ public WaveformScreen(Context context) {
+ super(context);
+ for (int i = 0; i < NUM_PARTICLES; i++) {
+ particles.add(new Particle(1.1f + 1.1f * i / NUM_PARTICLES - 0.05f, 0, 0, 0));
+ }
+ }
+
+ @Override
+ protected void drawParticle(@NonNull Canvas canvas, Particle p, float width, float height,
+ float innerBoxX, float innerBoxY, float innerBoxSize) {
+ float x = (float) (width * p.positionX);
+ canvas.drawRect(x, height, x + (1.1f * width) / NUM_PARTICLES,
+ (float) (0.95f * height - 0.3f * p.positionY * height), paintParticles);
+ }
+
+ @Override
+ protected void particleTick(Particle p, long timeSinceLastFrame) {
+ p.positionX += 0.0001 * timeSinceLastFrame;
+ if (p.positionY <= 0.2 || p.positionY >= 1) {
+ p.speed = -p.speed;
+ p.positionY -= p.speed * timeSinceLastFrame;
+ }
+ p.positionY -= p.speed * timeSinceLastFrame;
+ if (p.positionX > 1.05f) {
+ p.positionX -= 1.1;
+ p.positionY = 0.2 + 0.8 * Math.random();
+ p.speed = 0.0008 * Math.random() - 0.0004;
+ }
+ }
+}
diff --git a/ui/echo/src/main/res/drawable-nodpi/echo.png b/ui/echo/src/main/res/drawable-nodpi/echo.png
new file mode 100644
index 000000000..8f1e3a854
--- /dev/null
+++ b/ui/echo/src/main/res/drawable-nodpi/echo.png
Binary files differ
diff --git a/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png
new file mode 100644
index 000000000..20408eb74
--- /dev/null
+++ b/ui/echo/src/main/res/drawable-nodpi/logo_monochrome.png
Binary files differ
diff --git a/ui/echo/src/main/res/font/sarabun_regular.ttf b/ui/echo/src/main/res/font/sarabun_regular.ttf
new file mode 100644
index 000000000..50fa7076d
--- /dev/null
+++ b/ui/echo/src/main/res/font/sarabun_regular.ttf
Binary files differ
diff --git a/ui/echo/src/main/res/font/sarabun_semi_bold.ttf b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf
new file mode 100644
index 000000000..7b760ce10
--- /dev/null
+++ b/ui/echo/src/main/res/font/sarabun_semi_bold.ttf
Binary files differ
diff --git a/ui/echo/src/main/res/layout/echo_activity.xml b/ui/echo/src/main/res/layout/echo_activity.xml
new file mode 100644
index 000000000..3c5590d3a
--- /dev/null
+++ b/ui/echo/src/main/res/layout/echo_activity.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/echoImage"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <ImageView
+ android:id="@+id/echoProgressImage"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginTop="16dp"
+ android:importantForAccessibility="no"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp" />
+
+ <ImageView
+ android:id="@+id/closeButton"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_margin="16dp"
+ android:src="@drawable/ic_close_white"
+ android:contentDescription="@string/close_label"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/echoProgressImage" />
+
+ <ImageView
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_margin="16dp"
+ android:src="@drawable/logo_monochrome"
+ android:importantForAccessibility="no"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/echoProgressImage" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:padding="32dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/aboveLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="#ffffff"
+ android:fontFamily="@font/sarabun_regular"
+ app:fontFamily="@font/sarabun_regular"
+ tools:text="text above"
+ style="@style/TextAppearance.Material3.TitleLarge" />
+
+ <TextView
+ android:id="@+id/largeLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="#ffffff"
+ android:layout_marginVertical="8dp"
+ android:fontFamily="@font/sarabun_semi_bold"
+ app:fontFamily="@font/sarabun_semi_bold"
+ tools:text="large"
+ style="@style/TextAppearance.Material3.DisplayLarge"
+ tools:targetApi="p" />
+
+ <TextView
+ android:id="@+id/belowLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="#ffffff"
+ android:fontFamily="@font/sarabun_regular"
+ app:fontFamily="@font/sarabun_regular"
+ tools:text="text below"
+ style="@style/TextAppearance.Material3.TitleLarge" />
+
+ <TextView
+ android:id="@+id/smallLabel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="#ffffff"
+ android:textSize="16sp"
+ android:layout_marginTop="32dp"
+ android:fontFamily="@font/sarabun_regular"
+ app:fontFamily="@font/sarabun_regular"
+ tools:text="small" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/echoLogo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_margin="32dp"
+ android:src="@drawable/echo"
+ android:importantForAccessibility="no"
+ android:layout_alignParentBottom="true" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/shareButton"
+ android:layout_width="wrap_content"
+ android:layout_height="56dp"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="true"
+ android:layout_margin="32dp"
+ android:text="@string/share_label"
+ android:drawableLeft="@drawable/ic_share"
+ android:textColor="#fff"
+ android:contentDescription="@string/share_label"
+ style="@style/Widget.Material3.Button.OutlinedButton"
+ app:strokeColor="#fff"
+ tools:ignore="RtlHardcoded" />
+
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/ui/echo/src/main/res/values-de/echo-strings.xml b/ui/echo/src/main/res/values-de/echo-strings.xml
new file mode 100644
index 000000000..2f0c3da0d
--- /dev/null
+++ b/ui/echo/src/main/res/values-de/echo-strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
+ <string name="echo_home_header">Jahresrückblick</string>
+ <string name="echo_home_subtitle">Deine Lieblings-Podcasts und Statistiken aus dem letzten Jahr. Exklusiv auf deinem Telefon.</string>
+
+ <string name="echo_intro_your_year">Dein Podcast-Jahr</string>
+ <string name="echo_intro_in_podcasts"> </string>
+ <string name="echo_intro_locally">privat auf deinem Telefon generiert</string>
+
+ <string name="echo_hours_this_year">Dieses Jahr hast du</string>
+ <plurals name="echo_hours_podcasts">
+ <item quantity="one">Stunden an Episoden von %1$d Podcast abgespielt</item>
+ <item quantity="other">Stunden an Episoden von %1$d Podcasts abgespielt</item>
+ </plurals>
+
+ <string name="echo_queue_title_clean">Und du bist bereit, das neue Jahr frisch zu starten. In deiner Warteschlange liegen</string>
+ <string name="echo_queue_title_many">Und du hast dieses Jahr noch eine ganze Menge vor dir. In deiner Warteschlange liegen</string>
+ <plurals name="echo_queue_hours_waiting">
+ <item quantity="one">Stunden aus %1$d Podcast-Episode</item>
+ <item quantity="other">Stunden aus %1$d Podcast-Episoden</item>
+ </plurals>
+ <string name="echo_queue_hours_clean">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt</string>
+ <string name="echo_queue_hours_normal">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Du kannst frisch ins Jahr starten, wenn du einige Episoden überspringst.</string>
+ <string name="echo_queue_hours_much">Das sind jeden Tag ungefähr %1$s bevor %2$d beginnt. Moment, was?</string>
+
+ <string name="echo_listened_after_title">Wir haben uns angeschaut, wann Episoden veröffentlicht werden und wann du sie abspielst. Unsere Folgerung?</string>
+ <string name="echo_listened_after_comment_easy">Du bist entspannt</string>
+ <string name="echo_listened_after_time">Typischerweise spielst du eine Episode %1$s nach der Veröffentlichung ab.</string>
+ <string name="echo_listened_after_comment_addict">Du bist ein Podcast-Junkie</string>
+
+ <string name="echo_hoarder_title">Wir haben uns auch gefragt: hörst du die Podcasts an, die du abonniert hast?</string>
+ <string name="echo_hoarder_subtitle_hoarder">Wenn wir uns die Zahlen so anschauen, glauben wir, du hamsterst Podcasts</string>
+ <string name="echo_hoarder_comment_hoarder">Zahlen lügen nicht, sagt man. Du hast dieses Jahr nur %1$d%% deiner %2$d aktiven Abonnements abgespielt, also liegen wir wahrscheinlich richtig.</string>
+ <string name="echo_hoarder_subtitle_medium">Du hamsterst keine Podcasts</string>
+ <string name="echo_hoarder_comment_medium">Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wie wäre es damit, mal wieder \"%3$s\" anzuhören?</string>
+ <string name="echo_hoarder_subtitle_clean">Aufgeräumt!</string>
+ <string name="echo_hoarder_comment_clean">Du hast dieses Jahr %1$d%% deiner %2$d aktiven Abonnements abgespielt. Wetten, du hältst auch deinen Schreibtisch sauber?</string>
+
+ <string name="echo_thanks_large">Danke</string>
+ <string name="echo_thanks_we_are_glad_old">dass du dieses Jahr wieder dabei warst!\n\nDu hast deine erste Episode im %1$s abgespielt. Es ist uns eine Ehre, seitdem für dich da zu sein.</string>
+ <string name="echo_thanks_we_are_glad_new">dass du dich dieses Jahr für uns entschieden hast!\n\nEgal ob du von einer anderen App gekommen bist oder mit AntennaPod in Podcasts eingestiegen bist: Wir sind froh, dass du da bist!</string>
+ <string name="echo_thanks_now_favorite">Schauen wir uns jetzt noch deine Lieblings-Podcasts an…</string>
+
+ <string name="echo_share_heading">Meine Lieblings-Podcasts</string>
+ <string name="echo_share">Mein Podcast-Jahr %d. #AntennaPodEcho</string>
+</resources>
diff --git a/ui/echo/src/main/res/values-es/echo-strings.xml b/ui/echo/src/main/res/values-es/echo-strings.xml
new file mode 100644
index 000000000..44e81947b
--- /dev/null
+++ b/ui/echo/src/main/res/values-es/echo-strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation,PluralsCandidate">
+ <string name="echo_home_header">Revisa el año</string>
+ <string name="echo_home_subtitle">Tus mejores pódcasts y estadísticas del año pasado. Exclusivamente en tu teléfono.</string>
+
+ <string name="echo_intro_your_year">Tu año</string>
+ <string name="echo_intro_in_podcasts">en pódcasts</string>
+ <string name="echo_intro_locally">generado de forma privada en tu teléfono</string>
+
+ <string name="echo_hours_this_year">Este año has reproducido</string>
+ <plurals name="echo_hours_podcasts">
+ <item quantity="one">horas de episodios\nde %1$d pódcast</item>
+ <item quantity="other">horas de episodios\nde %1$d pódcasts diferentes</item>
+ </plurals>
+
+ <string name="echo_queue_title_clean">Y estás listo para empezar el año de nuevo. Tienes</string>
+ <string name="echo_queue_title_many">Y aún te queda bastante este año. Tienes</string>
+ <plurals name="echo_queue_hours_waiting">
+ <item quantity="one">hora esperando en tu cola\nde %1$d episodio</item>
+ <item quantity="other">horas esperando en tu cola\nde %1$d episodios</item>
+ </plurals>
+ <string name="echo_queue_hours_clean">Eso es alrededor de %1$s cada día hasta que empiece %2$d</string>
+ <string name="echo_queue_hours_normal">Eso son %1$s cada día hasta que empiece %2$d. Puedes empezar el año de cero si te saltas algunos episodios.</string>
+ <string name="echo_queue_hours_much">Eso son %1$s cada día hasta que empiece %2$d. Espera, ¿qué?</string>
+
+ <string name="echo_listened_after_title">Hemos analizado cuándo se publican los episodios y cuándo los completaste. ¿Nuestra conclusión?</string>
+ <string name="echo_listened_after_comment_easy">Eres relajado</string>
+ <string name="echo_listened_after_time">Normalmente, completaste un episodio %1$s después de su publicación.</string>
+ <string name="echo_listened_after_comment_addict">Eres un adicto a los pódcasts</string>
+
+ <string name="echo_hoarder_title">También nos hemos preguntado: ¿Escuchas los pódcasts a los que estás suscrito?</string>
+ <string name="echo_hoarder_subtitle_hoarder">Viendo los números, creemos que eres un acumulador</string>
+ <string name="echo_hoarder_comment_hoarder">Dicen que los números no mienten. Y con solo %1$d%% de tus %2$d suscripciones activas reproducidas este año, probablemente tengamos razón.</string>
+ <string name="echo_hoarder_subtitle_medium">Mira. Aquí no hay acumulación.</string>
+ <string name="echo_hoarder_comment_medium">Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¿Qué tal si vuelves a escuchar \"%3$s\"?</string>
+ <string name="echo_hoarder_subtitle_clean">¡Limpio!</string>
+ <string name="echo_hoarder_comment_clean">Has reproducido episodios de %1$d%% de tus %2$d suscripciones activas este año. ¡Apostamos que también mantienes limpio tu escritorio!</string>
+
+ <string name="echo_thanks_large">¡Gracias</string>
+ <string name="echo_thanks_we_are_glad_old">por estar con nosotros este año!\n\nReproduciste tu primer episodio con nosotros en %1$s. Ha sido un honor servirte desde entonces.</string>
+ <string name="echo_thanks_we_are_glad_new">por unirte a nosotros este año!\n\nWTanto si vienes de otra aplicación como si has empezado tu aventura con los pódcasts con nosotros, ¡Estamos encantados de tenerte!</string>
+ <string name="echo_thanks_now_favorite">Ahora, echemos un vistazo a tus pódcasts favoritos…</string>
+
+ <string name="echo_share_heading">Mis pódcasts favoritos</string>
+ <string name="echo_share">Mi año %d en pódcasts. #AntennaPodEcho</string>
+</resources>
diff --git a/ui/echo/src/main/res/values-fr/echo-strings.xml b/ui/echo/src/main/res/values-fr/echo-strings.xml
new file mode 100644
index 000000000..a5be76ef1
--- /dev/null
+++ b/ui/echo/src/main/res/values-fr/echo-strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
+ <string name="echo_home_header">Bilan de l\'année</string>
+ <string name="echo_home_subtitle">Vos podcasts et statistiques de cette année. Exclusivement sur votre téléphone.</string>
+
+ <string name="echo_intro_your_year">Votre année</string>
+ <string name="echo_intro_in_podcasts">de podcasts</string>
+ <string name="echo_intro_locally">généré par votre téléphone pour respecter votre vie privée</string>
+
+ <string name="echo_hours_this_year">Cette année vous avez écouté</string>
+ <plurals name="echo_hours_podcasts">
+ <item quantity="one">heures d\'épisodes\nd\'%1$d seul podcast</item>
+ <item quantity="many">heures d\'épisodes\nde %1$d podcasts différents</item>
+ <item quantity="other">heures d\'épisodes\nde %1$d podcasts différents</item>
+ </plurals>
+
+ <string name="echo_queue_title_clean">Et vous êtes prêt à finir à l\'année avec seulement</string>
+ <string name="echo_queue_title_many">Et vous avez de la réserve pour finir l\'année avec</string>
+ <plurals name="echo_queue_hours_waiting">
+ <item quantity="one">heures pour finir votre liste de lecture\nd\'%1$d seul épisode</item>
+ <item quantity="many">heures finir votre liste de lecture\nde %1$d épisodes</item>
+ <item quantity="other">heures finir votre liste de lecture\nde %1$d épisodes</item>
+ </plurals>
+ <string name="echo_queue_hours_clean">C\'est environ %1$s tous les jours avant que %2$d ne commence.</string>
+ <string name="echo_queue_hours_normal">C\'est environ %1$s tous les jours avant que %2$d ne commence. Si vous voulez y arriver des épisodes vont devoir être sautés !</string>
+ <string name="echo_queue_hours_much">C\'est environ %1$s tous les jours avant que %2$d ne commence. Hein !? Ça semble tendu !</string>
+
+ <string name="echo_listened_after_title">On a lancé quelques analyses entre le moment où un épisode est publié et son écoute. Notre conclusion ?</string>
+ <string name="echo_listened_after_comment_easy">Vous êtes zen</string>
+ <string name="echo_listened_after_time">En général, vous avez écouté un episode %1$s après sa publication.</string>
+ <string name="echo_listened_after_comment_addict">Vous êtes accro aux podcasts</string>
+
+ <string name="echo_hoarder_title">On s\'est aussi demandé si vous écoutiez les podcasts auxquels vous êtes abonné ?</string>
+ <string name="echo_hoarder_subtitle_hoarder">Au vu des chiffres on a détecté un petit syndrome de collectionnite !</string>
+ <string name="echo_hoarder_comment_hoarder">Et il parait que les chiffres ne mentent pas. Avec seulement %1$d%% de vos %2$d abonnements actifs ayant été lu cette année, nous avons probablement raison.</string>
+ <string name="echo_hoarder_subtitle_medium">Rien à signaler ! Pas de collectionnite aigüe détectée !</string>
+ <string name="echo_hoarder_comment_medium">Vous avez lu %1$d%% de vos %2$d abonnements actifs cette année. Pourquoi ne pas allez rejeter un coup d\'oeil à \"%3$s\" ?</string>
+ <string name="echo_hoarder_subtitle_clean">Nickel !</string>
+ <string name="echo_hoarder_comment_clean">Vous avez lu %1$d%% de vos %2$d abonnements actifs cette année. On parie que vous ête du genre à avoir un bureau propre !</string>
+
+ <string name="echo_thanks_large">Merci</string>
+ <string name="echo_thanks_we_are_glad_old">d\'avoir partagé cette année avec nous !\n\nVotre premier épisode avec nous a été lu en %1$s et on est fier de continuer à vous être utile.</string>
+ <string name="echo_thanks_we_are_glad_new">de nous avoir rejoint cette année !\n\nPeu importe si vous avez changé d\'application ou commencé à écouter les podcasts avec nous : on est content de vous avoir !</string>
+ <string name="echo_thanks_now_favorite">Maintenant, jetons un coup d\'oeil à vos podcasts préférés…</string>
+
+ <string name="echo_share_heading">Mes podcasts préférés</string>
+ <string name="echo_share">Mon année %d en podcasts. #AntennaPodEcho</string>
+</resources>
diff --git a/ui/echo/src/main/res/values-it/echo-strings.xml b/ui/echo/src/main/res/values-it/echo-strings.xml
new file mode 100644
index 000000000..ecc8949ee
--- /dev/null
+++ b/ui/echo/src/main/res/values-it/echo-strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
+ <string name="echo_home_header">Passa in rassegna l\'anno</string>
+ <string name="echo_home_subtitle">I tuoi podcast più ascoltati e statistiche sull\'anno appena trascorso. In esclusiva sul tuo telefono.</string>
+
+ <string name="echo_intro_your_year">Il tuo anno</string>
+ <string name="echo_intro_in_podcasts">in podcast</string>
+ <string name="echo_intro_locally">generato in privato sul tuo telefono</string>
+
+ <string name="echo_hours_this_year">Quest\'anno hai riprodotto</string>
+ <plurals name="echo_hours_podcasts">
+ <item quantity="one">ore di episodi\nda %1$d podcast</item>
+ <item quantity="other">ore di episodi\nda %1$d podcast diversi</item>
+ </plurals>
+
+ <string name="echo_queue_title_clean">E sei pronto a cominciare da zero il nuovo anno. Hai</string>
+ <string name="echo_queue_title_many">E ti resta ancora un bel po\' da fare quest\'anno. Hai</string>
+ <plurals name="echo_queue_hours_waiting">
+ <item quantity="one">ore provenienti da %1$d episodio\nin attesa nella tua coda</item>
+ <item quantity="other">ore provenienti da %1$d episodi\nin attesa nella tua coda</item>
+ </plurals>
+ <string name="echo_queue_hours_clean">Cioè circa %1$s al giorno da qui al %2$d.</string>
+ <string name="echo_queue_hours_normal">Cioè circa %1$s al giorno da qui al %2$d. Puoi cominciare da zero il nuovo anno se salti qualche episodio.</string>
+ <string name="echo_queue_hours_much">Cioè circa %1$s al giorno da qui al %2$d. Aspetta, come hai detto?</string>
+
+ <string name="echo_listened_after_title">Abbiamo analizzato quando gli episodi sono pubblicati e quando finisci di ascoltarli. La nostra conclusione?</string>
+ <string name="echo_listened_after_comment_easy">Te la prendi comoda</string>
+ <string name="echo_listened_after_time">In genere, finisci di ascoltare un episodio %1$s dopo la sua pubblicazione.</string>
+ <string name="echo_listened_after_comment_addict">Sei podcast-dipendente</string>
+
+ <string name="echo_hoarder_title">Ci siamo chiesti anche: ascolti davvero i podcast a cui sei iscritto?</string>
+ <string name="echo_hoarder_subtitle_hoarder">Dati alla mano, pensiamo che tu sia un accumulatore</string>
+ <string name="echo_hoarder_comment_hoarder">Si dice che i numeri non mentano. E visto che quest\'anno hai riprodotto solo il %1$d%% delle tue %2$d iscrizioni attive, probabilmente abbiamo ragione.</string>
+ <string name="echo_hoarder_subtitle_medium">Verificato. Non c\'è traccia di accumulo seriale.</string>
+ <string name="echo_hoarder_comment_medium">Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Che ne dici di ascoltare di nuovo \"%3$s\"?</string>
+ <string name="echo_hoarder_subtitle_clean">In ordine!</string>
+ <string name="echo_hoarder_comment_clean">Quest\'anno hai riprodotto episodi dal %1$d%% delle tue %2$d iscrizioni attive. Scommettiamo che tieni in ordine anche la tua scrivania!</string>
+
+ <string name="echo_thanks_large">Grazie</string>
+ <string name="echo_thanks_we_are_glad_old">per essere rimasto con noi quest\'anno!\n\nHai riprodotto il tuo primo episodio con noi in %1$s. Da allora, siamo onorati di essere al tuo servizio.</string>
+ <string name="echo_thanks_we_are_glad_new">per esserti unito a noi quest\'anno!\n\nChe tu sia arrivato qui da un\'altra app o che tu abbia iniziato la tua avventura coi podcast con noi, siamo felici di averti qui!</string>
+ <string name="echo_thanks_now_favorite">Ora diamo un\'occhiata ai tuoi podcast preferiti…</string>
+
+ <string name="echo_share_heading">I miei podcast preferiti</string>
+ <string name="echo_share">Il mio %d in podcast. #AntennaPodEcho</string>
+</resources>
diff --git a/ui/echo/src/main/res/values/echo-strings.xml b/ui/echo/src/main/res/values/echo-strings.xml
new file mode 100644
index 000000000..09a64f1e4
--- /dev/null
+++ b/ui/echo/src/main/res/values/echo-strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation,PluralsCandidate">
+ <string name="echo_home_header">Review the year</string>
+ <string name="echo_home_subtitle">Your top podcasts and stats from the past year. Exclusively on your phone.</string>
+
+ <string name="echo_intro_your_year">Your year</string>
+ <string name="echo_intro_in_podcasts">in podcasts</string>
+ <string name="echo_intro_locally">generated privately on your phone</string>
+
+ <string name="echo_hours_this_year">This year you played</string>
+ <plurals name="echo_hours_podcasts">
+ <item quantity="one">hours of episodes\nfrom %1$d podcast</item>
+ <item quantity="other">hours of episodes\nfrom %1$d different podcasts</item>
+ </plurals>
+
+ <string name="echo_queue_title_clean">And you\'re ready to make a clean start of the year. You have</string>
+ <string name="echo_queue_title_many">And you still have quite a bit to go this year. You have</string>
+ <plurals name="echo_queue_hours_waiting">
+ <item quantity="one">hours waiting in your queue\nfrom %1$d episode</item>
+ <item quantity="other">hours waiting in your queue\nfrom %1$d episodes</item>
+ </plurals>
+ <string name="echo_queue_hours_clean">That\'s about %1$s each day until %2$d starts.</string>
+ <string name="echo_queue_hours_normal">That\'s about %1$s each day until %2$d starts. You can start the year clean if you skip a few episodes.</string>
+ <string name="echo_queue_hours_much">That\'s about %1$s each day until %2$d starts. Wait, what?</string>
+
+ <string name="echo_listened_after_title">We\'ve run some analysis on when episodes are released, and when you completed them. Our conclusion?</string>
+ <string name="echo_listened_after_emoji_yoga" translatable="false">\uD83E\uDDD8</string>
+ <string name="echo_listened_after_comment_easy">You\'re easy going</string>
+ <string name="echo_listened_after_time">Typically, you completed an episode %1$s after it was released.</string>
+ <string name="echo_listened_after_emoji_run" translatable="false">\uD83C\uDFC3</string>
+ <string name="echo_listened_after_comment_addict">You\'re a podcast addict</string>
+
+ <string name="echo_hoarder_title">We\'ve also been wondering: do you listen to the podcasts that you\'re subscribed to?</string>
+ <string name="echo_hoarder_subtitle_hoarder">Looking at the numbers, we think you\'re a hoarder</string>
+ <string name="echo_hoarder_emoji_cabinet" translatable="false">\uD83D\uDDC4\uFE0F</string>
+ <string name="echo_hoarder_comment_hoarder">Numbers don\'t lie, they say. And with only %1$d%% of your %2$d active subscriptions having been played this year, we\'re probably right.</string>
+ <string name="echo_hoarder_subtitle_medium">Check. No hoarding here.</string>
+ <string name="echo_hoarder_emoji_check" translatable="false">\u2705</string>
+ <string name="echo_hoarder_comment_medium">You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. How about checking out \"%3$s\" again?</string>
+ <string name="echo_hoarder_subtitle_clean">Clean!</string>
+ <string name="echo_hoarder_emoji_clean" translatable="false">\u2728</string>
+ <string name="echo_hoarder_comment_clean">You\'ve played episodes from %1$d%% of your %2$d active subscriptions this year. We bet you keep your desk clean, too!</string>
+
+ <string name="echo_thanks_large">Thanks</string>
+ <string name="echo_thanks_we_are_glad_old">for sticking with us this year!\n\nYou played your first episode with us in %1$s. It\'s been our honor to serve you since.</string>
+ <string name="echo_thanks_we_are_glad_new">for joining us this year!\n\nWhether you\'ve moved over from another app, or started your podcast adventure with us: we\'re glad to have you!</string>
+ <string name="echo_thanks_now_favorite">Now, let\'s take a look at your favorite podcasts…</string>
+
+ <string name="echo_share_heading">My favorite podcasts</string>
+ <string name="echo_share">My year %d in podcasts. #AntennaPodEcho</string>
+</resources>
diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml
index 493e8672c..74e979213 100644
--- a/ui/i18n/src/main/res/values/strings.xml
+++ b/ui/i18n/src/main/res/values/strings.xml
@@ -27,6 +27,8 @@
<string name="years_statistics_label">Years</string>
<string name="notification_pref_fragment">Notifications</string>
<string name="recently_played_episodes">Recently played episodes</string>
+ <string name="antennapod_echo" translatable="false">AntennaPod Echo</string>
+ <string name="antennapod_echo_year" translatable="false">AntennaPod Echo %d</string>
<!-- Google Assistant -->
<string name="app_action_not_found">\"%1$s\" not found</string>
@@ -620,6 +622,10 @@
<item quantity="one">1 hour</item>
<item quantity="other">%d hours</item>
</plurals>
+ <plurals name="time_days_quantified">
+ <item quantity="one">1 day</item>
+ <item quantity="other">%d days</item>
+ </plurals>
<string name="auto_enable_label">Automatically activate the sleep timer when pressing play</string>
<string name="auto_enable_label_with_times">Automatically activate the sleep timer when pressing play between %s and %s</string>
<string name="auto_enable_change_times">Change time range</string>