summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2023-11-28 20:26:29 +0100
committerGitHub <noreply@github.com>2023-11-28 20:26:29 +0100
commitee554d0306a06903fa88be6c5af7954315685ed6 (patch)
tree322325e0a3929b4a6abacea0ff221c3bff24f05c /ui
parent637230e382a67ae8a0c7d889a773edb64bb74181 (diff)
downloadAntennaPod-ee554d0306a06903fa88be6c5af7954315685ed6.zip
AntennaPod Echo (#6780)
Diffstat (limited to 'ui')
-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
22 files changed, 1224 insertions, 0 deletions
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>