summaryrefslogtreecommitdiff
path: root/ui/widget
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2024-03-25 23:45:09 +0100
committerGitHub <noreply@github.com>2024-03-25 23:45:09 +0100
commit130da46f5d9c598ca76f4f2127c08bb4ef225b34 (patch)
tree39007501eeecf0dd188d6f4fa57cd3c6dac6e4b0 /ui/widget
parent160089d3fffeb788464e23b36bc476a3e0cde183 (diff)
downloadAntennaPod-130da46f5d9c598ca76f4f2127c08bb4ef225b34.zip
Move widget setup code to widget module (#6996)
Diffstat (limited to 'ui/widget')
-rw-r--r--ui/widget/build.gradle16
-rw-r--r--ui/widget/src/main/AndroidManifest.xml27
-rw-r--r--ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/PlayerWidget.java98
-rw-r--r--ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetConfigActivity.java145
-rw-r--r--ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdater.java226
-rw-r--r--ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdaterWorker.java58
-rw-r--r--ui/widget/src/main/res/drawable-hdpi/ic_widget_preview.pngbin0 -> 19686 bytes
-rw-r--r--ui/widget/src/main/res/layout/activity_widget_config.xml124
-rw-r--r--ui/widget/src/main/res/layout/player_widget.xml153
-rw-r--r--ui/widget/src/main/res/values/dimens.xml5
-rw-r--r--ui/widget/src/main/res/xml/player_widget_info.xml12
11 files changed, 864 insertions, 0 deletions
diff --git a/ui/widget/build.gradle b/ui/widget/build.gradle
index 2488054a6..03183a323 100644
--- a/ui/widget/build.gradle
+++ b/ui/widget/build.gradle
@@ -2,6 +2,7 @@ plugins {
id("com.android.library")
}
apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
android {
namespace "de.danoeh.antennapod.ui.widget"
@@ -10,10 +11,25 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = ["xhdpi"]
}
+
+ lint {
+ disable "IconMissingDensityFolder"
+ }
}
dependencies {
+ implementation project(":model")
+ implementation project(":playback:base")
+ implementation project(':storage:preferences')
+ implementation project(':storage:database')
+ implementation project(":ui:app-start-intent")
implementation project(":ui:common")
+ implementation project(":ui:episodes")
+ implementation project(':ui:glide')
+ implementation project(':ui:i18n')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "androidx.work:work-runtime:$workManagerVersion"
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
}
diff --git a/ui/widget/src/main/AndroidManifest.xml b/ui/widget/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c0f03b6cc
--- /dev/null
+++ b/ui/widget/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <activity
+ android:name=".WidgetConfigActivity"
+ android:label="@string/widget_settings"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
+ </intent-filter>
+ </activity>
+
+ <receiver
+ android:name=".PlayerWidget"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ <action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/>
+ <action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/player_widget_info"/>
+ </receiver>
+ </application>
+</manifest>
diff --git a/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/PlayerWidget.java b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/PlayerWidget.java
new file mode 100644
index 000000000..c548c075a
--- /dev/null
+++ b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/PlayerWidget.java
@@ -0,0 +1,98 @@
+package de.danoeh.antennapod.ui.widget;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+public class PlayerWidget extends AppWidgetProvider {
+ private static final String TAG = "PlayerWidget";
+ public static final String PREFS_NAME = "PlayerWidgetPrefs";
+ private static final String KEY_WORKAROUND_ENABLED = "WorkaroundEnabled";
+ private static final String KEY_ENABLED = "WidgetEnabled";
+ public static final String KEY_WIDGET_COLOR = "widget_color";
+ public static final String KEY_WIDGET_PLAYBACK_SPEED = "widget_playback_speed";
+ public static final String KEY_WIDGET_SKIP = "widget_skip";
+ public static final String KEY_WIDGET_FAST_FORWARD = "widget_fast_forward";
+ public static final String KEY_WIDGET_REWIND = "widget_rewind";
+ public static final int DEFAULT_COLOR = 0xff262C31;
+ private static final String WORKAROUND_WORK_NAME = "WidgetUpdaterWorkaround";
+
+ @Override
+ public void onEnabled(Context context) {
+ super.onEnabled(context);
+ Log.d(TAG, "Widget enabled");
+ setEnabled(context, true);
+ WidgetUpdaterWorker.enqueueWork(context);
+ scheduleWorkaround(context);
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = ["
+ + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
+ WidgetUpdaterWorker.enqueueWork(context);
+
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ if (!prefs.getBoolean(KEY_WORKAROUND_ENABLED, false)) {
+ scheduleWorkaround(context);
+ prefs.edit().putBoolean(KEY_WORKAROUND_ENABLED, true).apply();
+ }
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+ super.onDisabled(context);
+ Log.d(TAG, "Widget disabled");
+ setEnabled(context, false);
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+ Log.d(TAG, "OnDeleted");
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ for (int appWidgetId : appWidgetIds) {
+ prefs.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_PLAYBACK_SPEED + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply();
+ }
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] widgetIds = manager.getAppWidgetIds(new ComponentName(context, PlayerWidget.class));
+ if (widgetIds.length == 0) {
+ prefs.edit().putBoolean(KEY_WORKAROUND_ENABLED, false).apply();
+ WorkManager.getInstance(context).cancelUniqueWork(WORKAROUND_WORK_NAME);
+ }
+ super.onDeleted(context, appWidgetIds);
+ }
+
+ private static void scheduleWorkaround(Context context) {
+ // Enqueueing work enables a BOOT_COMPLETED receiver, which in turn makes Android refresh widgets.
+ // This creates an endless loop with a flickering widget.
+ // Workaround: When there is a widget, schedule a dummy task in the far future, so that the receiver stays.
+ final OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(WidgetUpdaterWorker.class)
+ .setInitialDelay(100 * 356, TimeUnit.DAYS)
+ .build();
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(WORKAROUND_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest);
+ }
+
+ public static boolean isEnabled(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ return prefs.getBoolean(KEY_ENABLED, false);
+ }
+
+ private void setEnabled(Context context, boolean enabled) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ENABLED, enabled).apply();
+ }
+}
diff --git a/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetConfigActivity.java b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetConfigActivity.java
new file mode 100644
index 000000000..83fd308ba
--- /dev/null
+++ b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetConfigActivity.java
@@ -0,0 +1,145 @@
+package de.danoeh.antennapod.ui.widget;
+
+import android.app.Activity;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import de.danoeh.antennapod.ui.common.ThemeSwitcher;
+
+import java.util.Locale;
+
+public class WidgetConfigActivity extends AppCompatActivity {
+ private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+ private SeekBar opacitySeekBar;
+ private TextView opacityTextView;
+ private View widgetPreview;
+ private CheckBox ckPlaybackSpeed;
+ private CheckBox ckRewind;
+ private CheckBox ckFastForward;
+ private CheckBox ckSkip;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(ThemeSwitcher.getTheme(this));
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_widget_config);
+
+ Intent configIntent = getIntent();
+ Bundle extras = configIntent.getExtras();
+ if (extras != null) {
+ appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+
+ Intent resultValue = new Intent();
+ resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ setResult(Activity.RESULT_CANCELED, resultValue);
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ finish();
+ }
+
+ opacityTextView = findViewById(R.id.widget_opacity_textView);
+ opacitySeekBar = findViewById(R.id.widget_opacity_seekBar);
+ widgetPreview = findViewById(R.id.widgetLayout);
+ findViewById(R.id.butConfirm).setOnClickListener(v -> confirmCreateWidget());
+ opacitySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+ opacityTextView.setText(String.format(Locale.getDefault(), "%d%%", seekBar.getProgress()));
+ int color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
+ widgetPreview.setBackgroundColor(color);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ });
+
+ widgetPreview.findViewById(R.id.txtNoPlaying).setVisibility(View.GONE);
+ TextView title = widgetPreview.findViewById(R.id.txtvTitle);
+ title.setVisibility(View.VISIBLE);
+ title.setText(R.string.app_name);
+ TextView progress = widgetPreview.findViewById(R.id.txtvProgress);
+ progress.setVisibility(View.VISIBLE);
+ progress.setText(R.string.position_default_label);
+
+ ckPlaybackSpeed = findViewById(R.id.ckPlaybackSpeed);
+ ckPlaybackSpeed.setOnClickListener(v -> displayPreviewPanel());
+ ckRewind = findViewById(R.id.ckRewind);
+ ckRewind.setOnClickListener(v -> displayPreviewPanel());
+ ckFastForward = findViewById(R.id.ckFastForward);
+ ckFastForward.setOnClickListener(v -> displayPreviewPanel());
+ ckSkip = findViewById(R.id.ckSkip);
+ ckSkip.setOnClickListener(v -> displayPreviewPanel());
+
+ setInitialState();
+ }
+
+ private void setInitialState() {
+ SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
+ ckPlaybackSpeed.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false));
+ ckRewind.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, false));
+ ckFastForward.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, false));
+ ckSkip.setChecked(prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, false));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ int color = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, PlayerWidget.DEFAULT_COLOR);
+ int opacity = Color.alpha(color) * 100 / 0xFF;
+
+ opacitySeekBar.setProgress(opacity, false);
+ }
+ displayPreviewPanel();
+ }
+
+ private void displayPreviewPanel() {
+ boolean showExtendedPreview =
+ ckPlaybackSpeed.isChecked() || ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked();
+ widgetPreview.findViewById(R.id.extendedButtonsContainer)
+ .setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE);
+ widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE);
+ widgetPreview.findViewById(R.id.butPlaybackSpeed)
+ .setVisibility(ckPlaybackSpeed.isChecked() ? View.VISIBLE : View.GONE);
+ widgetPreview.findViewById(R.id.butFastForward)
+ .setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE);
+ widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE);
+ widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE);
+ }
+
+ private void confirmCreateWidget() {
+ int backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.getProgress());
+
+ SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor);
+ editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked());
+ editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked());
+ editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked());
+ editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked());
+ editor.apply();
+
+ Intent resultValue = new Intent();
+ resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ setResult(Activity.RESULT_OK, resultValue);
+ finish();
+ WidgetUpdaterWorker.enqueueWork(this);
+ }
+
+ private int getColorWithAlpha(int color, int opacity) {
+ return (int) Math.round(0xFF * (0.01 * opacity)) * 0x1000000 + (color & 0xffffff);
+ }
+}
diff --git a/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdater.java b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdater.java
new file mode 100644
index 000000000..bb62d4a7b
--- /dev/null
+++ b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdater.java
@@ -0,0 +1,226 @@
+package de.danoeh.antennapod.ui.widget;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.bitmap.FitCenter;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.request.RequestOptions;
+
+import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter;
+import de.danoeh.antennapod.ui.common.Converter;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.model.playback.MediaType;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.PlaybackSpeedActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
+import de.danoeh.antennapod.ui.episodes.ImageResourceUtils;
+import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter;
+
+/**
+ * Updates the state of the player widget.
+ */
+public abstract class WidgetUpdater {
+ private static final String TAG = "WidgetUpdater";
+
+ public static class WidgetState {
+ final Playable media;
+ final PlayerStatus status;
+ final int position;
+ final int duration;
+ final float playbackSpeed;
+
+ public WidgetState(Playable media, PlayerStatus status, int position, int duration, float playbackSpeed) {
+ this.media = media;
+ this.status = status;
+ this.position = position;
+ this.duration = duration;
+ this.playbackSpeed = playbackSpeed;
+ }
+
+ public WidgetState(PlayerStatus status) {
+ this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f);
+ }
+ }
+
+ /**
+ * Update the widgets with the given parameters. Must be called in a background thread.
+ */
+ public static void updateWidget(Context context, WidgetState widgetState) {
+ if (!PlayerWidget.isEnabled(context) || widgetState == null) {
+ return;
+ }
+
+ PendingIntent startMediaPlayer;
+ if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO) {
+ startMediaPlayer = new VideoPlayerActivityStarter(context).getPendingIntent();
+ } else {
+ startMediaPlayer = new MainActivityStarter(context).withOpenPlayer().getPendingIntent();
+ }
+
+ PendingIntent startPlaybackSpeedDialog = new PlaybackSpeedActivityStarter(context).getPendingIntent();
+
+ RemoteViews views;
+ views = new RemoteViews(context.getPackageName(), R.layout.player_widget);
+
+ if (widgetState.media != null) {
+ Bitmap icon;
+ int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlaybackSpeed, startPlaybackSpeedDialog);
+
+ int radius = context.getResources().getDimensionPixelSize(R.dimen.widget_inner_radius);
+ RequestOptions options = new RequestOptions()
+ .dontAnimate()
+ .transform(new FitCenter(), new RoundedCorners(radius));
+
+ try {
+ icon = Glide.with(context)
+ .asBitmap()
+ .load(widgetState.media.getImageLocation())
+ .apply(options)
+ .submit(iconSize, iconSize)
+ .get(500, TimeUnit.MILLISECONDS);
+ views.setImageViewBitmap(R.id.imgvCover, icon);
+ } catch (Throwable tr1) {
+ try {
+ icon = Glide.with(context)
+ .asBitmap()
+ .load(ImageResourceUtils.getFallbackImageLocation(widgetState.media))
+ .apply(options)
+ .submit(iconSize, iconSize)
+ .get(500, TimeUnit.MILLISECONDS);
+ views.setImageViewBitmap(R.id.imgvCover, icon);
+ } catch (Throwable tr2) {
+ Log.e(TAG, "Error loading the media icon for the widget", tr2);
+ views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher);
+ }
+ }
+
+ views.setTextViewText(R.id.txtvTitle, widgetState.media.getEpisodeTitle());
+ views.setViewVisibility(R.id.txtvTitle, View.VISIBLE);
+ views.setViewVisibility(R.id.txtNoPlaying, View.GONE);
+
+ String progressString = getProgressString(widgetState.position,
+ widgetState.duration, widgetState.playbackSpeed);
+ if (progressString != null) {
+ views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
+ views.setTextViewText(R.id.txtvProgress, progressString);
+ }
+
+ if (widgetState.status == PlayerStatus.PLAYING) {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_pause);
+ views.setContentDescription(R.id.butPlay, context.getString(R.string.pause_label));
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_pause);
+ views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.pause_label));
+ } else {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_play);
+ views.setContentDescription(R.id.butPlay, context.getString(R.string.play_label));
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play);
+ views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label));
+ }
+ views.setOnClickPendingIntent(R.id.butPlay,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setOnClickPendingIntent(R.id.butPlayExtended,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setOnClickPendingIntent(R.id.butRew,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND));
+ views.setOnClickPendingIntent(R.id.butFastForward,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD));
+ views.setOnClickPendingIntent(R.id.butSkip,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT));
+ } else {
+ // start the app if they click anything
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlayExtended,
+ MediaButtonStarter.createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setViewVisibility(R.id.txtvProgress, View.GONE);
+ views.setViewVisibility(R.id.txtvTitle, View.GONE);
+ views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE);
+ views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher);
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_play);
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play);
+ }
+
+ ComponentName playerWidget = new ComponentName(context, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] widgetIds = manager.getAppWidgetIds(playerWidget);
+
+ for (int id : widgetIds) {
+ Bundle options = manager.getAppWidgetOptions(id);
+ SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
+ int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
+ int columns = getCellsForSize(minWidth);
+ if (columns < 3) {
+ views.setViewVisibility(R.id.layout_center, View.INVISIBLE);
+ } else {
+ views.setViewVisibility(R.id.layout_center, View.VISIBLE);
+ }
+ boolean showPlaybackSpeed = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + id, false);
+ boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false);
+ boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false);
+ boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false);
+
+ if (showPlaybackSpeed || showRewind || showSkip || showFastForward) {
+ views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE);
+ views.setInt(R.id.butPlay, "setVisibility", View.GONE);
+ views.setInt(R.id.butPlaybackSpeed, "setVisibility", showPlaybackSpeed ? View.VISIBLE : View.GONE);
+ views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE);
+ views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE);
+ views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE);
+ } else {
+ views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.GONE);
+ views.setInt(R.id.butPlay, "setVisibility", View.VISIBLE);
+ }
+
+ int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR);
+ views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor);
+
+ manager.updateAppWidget(id, views);
+ }
+ }
+
+ /**
+ * Returns number of cells needed for given size of the widget.
+ *
+ * @param size Widget size in dp.
+ * @return Size in number of cells.
+ */
+ private static int getCellsForSize(int size) {
+ int n = 2;
+ while (70 * n - 30 < size) {
+ ++n;
+ }
+ return n - 1;
+ }
+
+ private static String getProgressString(int position, int duration, float speed) {
+ if (position < 0 || duration <= 0) {
+ return null;
+ }
+ TimeSpeedConverter converter = new TimeSpeedConverter(speed);
+ if (UserPreferences.shouldShowRemainingTime()) {
+ return Converter.getDurationStringLong(converter.convert(position)) + " / -"
+ + Converter.getDurationStringLong(converter.convert(Math.max(0, duration - position)));
+ } else {
+ return Converter.getDurationStringLong(converter.convert(position)) + " / "
+ + Converter.getDurationStringLong(converter.convert(duration));
+ }
+ }
+}
diff --git a/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdaterWorker.java b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdaterWorker.java
new file mode 100644
index 000000000..97b0790ac
--- /dev/null
+++ b/ui/widget/src/main/java/de/danoeh/antennapod/ui/widget/WidgetUpdaterWorker.java
@@ -0,0 +1,58 @@
+package de.danoeh.antennapod.ui.widget;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils;
+
+public class WidgetUpdaterWorker extends Worker {
+
+ private static final String TAG = "WidgetUpdaterWorker";
+
+ public WidgetUpdaterWorker(@NonNull final Context context,
+ @NonNull final WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ public static void enqueueWork(final Context context) {
+ final OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(WidgetUpdaterWorker.class).build();
+ WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, workRequest);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ try {
+ updateWidget();
+ } catch (final Exception e) {
+ Log.d(TAG, "Failed to update AntennaPod widget: ", e);
+ return Result.failure();
+ }
+ return Result.success();
+ }
+
+ /**
+ * Loads the current media from the database and updates the widget in a background job.
+ */
+ private void updateWidget() {
+ final Playable media = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId());
+ if (media != null) {
+ WidgetUpdater.updateWidget(getApplicationContext(),
+ new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED,
+ media.getPosition(), media.getDuration(),
+ PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)));
+ } else {
+ WidgetUpdater.updateWidget(getApplicationContext(),
+ new WidgetUpdater.WidgetState(PlayerStatus.STOPPED));
+ }
+ }
+}
diff --git a/ui/widget/src/main/res/drawable-hdpi/ic_widget_preview.png b/ui/widget/src/main/res/drawable-hdpi/ic_widget_preview.png
new file mode 100644
index 000000000..3c1e08a31
--- /dev/null
+++ b/ui/widget/src/main/res/drawable-hdpi/ic_widget_preview.png
Binary files differ
diff --git a/ui/widget/src/main/res/layout/activity_widget_config.xml b/ui/widget/src/main/res/layout/activity_widget_config.xml
new file mode 100644
index 000000000..04be2dfa2
--- /dev/null
+++ b/ui/widget/src/main/res/layout/activity_widget_config.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="de.danoeh.antennapod.ui.widget.WidgetConfigActivity">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:layout_gravity="center">
+
+ <ImageView
+ android:id="@+id/widget_config_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ app:srcCompat="@drawable/teaser" />
+
+ <include
+ android:id="@+id/widget_config_preview"
+ android:layout_width="match_parent"
+ android:layout_height="96dp"
+ android:layout_gravity="center"
+ android:layout_margin="16dp"
+ layout="@layout/player_widget" />
+
+ </FrameLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/widget_opacity"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/widget_opacity_textView"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="end"
+ android:text="100%"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ </LinearLayout>
+
+ <SeekBar
+ android:id="@+id/widget_opacity_seekBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:max="100"
+ android:progress="100" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/ckPlaybackSpeed"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/playback_speed" />
+
+ <CheckBox
+ android:id="@+id/ckRewind"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/rewind_label" />
+
+ <CheckBox
+ android:id="@+id/ckFastForward"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/fast_forward_label" />
+
+ <CheckBox
+ android:id="@+id/ckSkip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/skip_episode_label" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/butConfirm"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/widget_create_button" />
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
+
+</LinearLayout>
diff --git a/ui/widget/src/main/res/layout/player_widget.xml b/ui/widget/src/main/res/layout/player_widget.xml
new file mode 100644
index 000000000..616e61e91
--- /dev/null
+++ b/ui/widget/src/main/res/layout/player_widget.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/widget_margin">
+
+ <RelativeLayout
+ android:id="@+id/widgetLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#262C31"
+ tools:ignore="UselessParent">
+
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:layout_width="@android:dimen/app_icon_size"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/play_label"
+ android:layout_alignParentEnd="true"
+ android:layout_margin="12dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:scaleType="fitCenter"
+ android:padding="8dp"
+ android:src="@drawable/ic_widget_play" />
+
+ <LinearLayout
+ android:id="@+id/layout_left"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/butPlay"
+ android:background="@android:color/transparent"
+ android:gravity="fill_horizontal"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:layout_width="@android:dimen/app_icon_size"
+ android:layout_height="match_parent"
+ android:src="@mipmap/ic_launcher"
+ android:importantForAccessibility="no"
+ android:layout_margin="12dp" />
+
+ <LinearLayout
+ android:id="@+id/layout_center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/txtNoPlaying"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="3"
+ android:text="@string/no_media_playing_label"
+ android:textColor="@color/white"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textColor="@color/white"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/txtvProgress"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:textColor="@color/white"
+ android:textSize="14sp"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/extendedButtonsContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageButton
+ android:id="@+id/butPlaybackSpeed"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/playback_speed"
+ android:layout_marginEnd="2dp"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_widget_playback_speed" />
+
+ <ImageButton
+ android:id="@+id/butRew"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/rewind_label"
+ android:layout_marginEnd="2dp"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_widget_fast_rewind" />
+
+ <ImageButton
+ android:id="@+id/butPlayExtended"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/play_label"
+ android:layout_marginEnd="2dp"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_widget_play" />
+
+ <ImageButton
+ android:id="@+id/butFastForward"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/fast_forward_label"
+ android:layout_marginEnd="2dp"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_widget_fast_forward" />
+
+ <ImageButton
+ android:id="@+id/butSkip"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/skip_episode_label"
+ android:layout_marginEnd="2dp"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_widget_skip" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ </RelativeLayout>
+
+</FrameLayout>
diff --git a/ui/widget/src/main/res/values/dimens.xml b/ui/widget/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..d0c504ac5
--- /dev/null
+++ b/ui/widget/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="widget_margin">0dp</dimen>
+ <dimen name="widget_inner_radius">4dp</dimen>
+</resources>
diff --git a/ui/widget/src/main/res/xml/player_widget_info.xml b/ui/widget/src/main/res/xml/player_widget_info.xml
new file mode 100644
index 000000000..0bbec5dda
--- /dev/null
+++ b/ui/widget/src/main/res/xml/player_widget_info.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:resizeMode="horizontal"
+ android:initialLayout="@layout/player_widget"
+ android:updatePeriodMillis="86400000"
+ android:previewImage="@drawable/ic_widget_preview"
+ android:minHeight="40dp"
+ android:minWidth="250dp"
+ android:minResizeWidth="40dp"
+ android:widgetFeatures="reconfigurable"
+ android:configure="de.danoeh.antennapod.ui.widget.WidgetConfigActivity">
+</appwidget-provider>