Merge pull request #4900 from ByteHamster/decouple-widget

Reduce coupling between widget and playback service
This commit is contained in:
ByteHamster 2021-02-03 23:56:33 +01:00 committed by GitHub
commit 9924952e2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 146 additions and 151 deletions

View File

@ -6,6 +6,7 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.LargeTest;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import org.awaitility.Awaitility;
import org.greenrobot.eventbus.EventBus;
import org.junit.After;
@ -187,8 +188,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -248,8 +249,9 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
countDownLatch.countDown();
return null;
}
@Override
@ -348,8 +350,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -391,8 +393,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override
@ -449,8 +451,8 @@ public class PlaybackServiceTaskManagerTest {
}
@Override
public void onWidgetUpdaterTick() {
public WidgetUpdater.WidgetState requestWidgetState() {
return null;
}
@Override

View File

@ -19,7 +19,7 @@ import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
public class WidgetConfigActivity extends AppCompatActivity {
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@ -127,7 +127,7 @@ public class WidgetConfigActivity extends AppCompatActivity {
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, resultValue);
finish();
PlayerWidgetJobService.updateWidget(this);
WidgetUpdaterJobService.performBackgroundUpdate(this);
}
private int getColorWithAlpha(int color, int opacity) {

View File

@ -45,6 +45,11 @@
android:label="@string/feed_update_receiver_name"
android:exported="true"
tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps -->
<service
android:name=".widget.WidgetUpdaterJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application>
</manifest>

View File

@ -9,7 +9,7 @@ import android.util.Log;
import java.util.Arrays;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
public class PlayerWidget extends AppWidgetProvider {
private static final String TAG = "PlayerWidget";
@ -25,7 +25,7 @@ public class PlayerWidget extends AppWidgetProvider {
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
super.onReceive(context, intent);
PlayerWidgetJobService.updateWidget(context);
WidgetUpdaterJobService.performBackgroundUpdate(context);
}
@Override
@ -33,13 +33,14 @@ public class PlayerWidget extends AppWidgetProvider {
super.onEnabled(context);
Log.d(TAG, "Widget enabled");
setEnabled(context, true);
PlayerWidgetJobService.updateWidget(context);
WidgetUpdaterJobService.performBackgroundUpdate(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) + "]");
PlayerWidgetJobService.updateWidget(context);
Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = ["
+ appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
WidgetUpdaterJobService.performBackgroundUpdate(context);
}
@Override

View File

@ -69,7 +69,6 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
@ -81,6 +80,7 @@ import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -794,8 +794,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
public void onWidgetUpdaterTick() {
PlayerWidgetJobService.updateWidget(getBaseContext());
public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed());
}
@Override
@ -866,9 +867,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
IntentUtils.sendLocalBroadcast(getApplicationContext(), ACTION_PLAYER_STATUS_CHANGED);
PlayerWidgetJobService.updateWidget(getBaseContext());
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
taskManager.requestWidgetUpdate();
}
@Override

View File

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import android.util.Log;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import io.reactivex.disposables.Disposable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@ -199,17 +200,28 @@ public class PlaybackServiceTaskManager {
*/
public synchronized void startWidgetUpdater() {
if (!isWidgetUpdaterActive() && !schedExecutor.isShutdown()) {
Runnable widgetUpdater = callback::onWidgetUpdaterTick;
Runnable widgetUpdater = this::requestWidgetUpdate;
widgetUpdater = useMainThreadIfNecessary(widgetUpdater);
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater,
WIDGET_UPDATER_NOTIFICATION_INTERVAL, WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
Log.d(TAG, "Started WidgetUpdater");
} else {
Log.d(TAG, "Call to startWidgetUpdater was ignored.");
}
}
/**
* Retrieves information about the widget state in the calling thread and then displays it in a background thread.
*/
public synchronized void requestWidgetUpdate() {
WidgetUpdater.WidgetState state = callback.requestWidgetState();
if (!schedExecutor.isShutdown()) {
schedExecutor.execute(() -> WidgetUpdater.updateWidget(context, state));
} else {
Log.d(TAG, "Call to requestWidgetUpdate was ignored.");
}
}
/**
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
* cancelled first.
@ -464,7 +476,7 @@ public class PlaybackServiceTaskManager {
void onSleepTimerReset();
void onWidgetUpdaterTick();
WidgetUpdater.WidgetState requestWidgetState();
void onChapterLoaded(Playable media);
}

View File

@ -21,6 +21,7 @@ import java.util.List;
*/
public interface Playable extends Parcelable,
ShownotesProvider, ImageResource {
public static final int INVALID_TIME = -1;
/**
* Save information about the playable in a preference so that it can be

View File

@ -1,17 +1,13 @@
package de.danoeh.antennapod.core.service;
package de.danoeh.antennapod.core.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.core.app.SafeJobIntentService;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
@ -24,7 +20,6 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
@ -35,107 +30,65 @@ import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.playback.Playable;
/**
* Updates the state of the player widget
* Updates the state of the player widget.
*/
public class PlayerWidgetJobService extends SafeJobIntentService {
public abstract class WidgetUpdater {
private static final String TAG = "WidgetUpdater";
private static final String TAG = "PlayerWidgetJobService";
public static class WidgetState {
final Playable media;
final PlayerStatus status;
final int position;
final int duration;
final float playbackSpeed;
private PlaybackService playbackService;
private final Object waitForService = new Object();
private final Object waitUsingService = new Object();
private static final int JOB_ID = -17001;
public static void updateWidget(Context context) {
enqueueWork(context, PlayerWidgetJobService.class, JOB_ID, new Intent(context, PlayerWidgetJobService.class));
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
if (!PlayerWidget.isEnabled(getApplicationContext())) {
return;
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;
}
synchronized (waitForService) {
if (PlaybackService.isRunning && playbackService == null) {
bindService(new Intent(this, PlaybackService.class), mConnection, 0);
while (playbackService == null) {
try {
waitForService.wait();
} catch (InterruptedException e) {
return;
}
}
}
}
synchronized (waitUsingService) {
updateViews();
}
if (playbackService != null) {
try {
unbindService(mConnection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "IllegalArgumentException when trying to unbind service");
}
public WidgetState(PlayerStatus status) {
this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f);
}
}
/**
* Returns number of cells needed for given size of the widget.
*
* @param size Widget size in dp.
* @return Size in number of cells.
* Update the widgets with the given parameters. Must be called in a background thread.
*/
private static int getCellsForSize(int size) {
int n = 2;
while (70 * n - 30 < size) {
++n;
public static void updateWidget(Context context, WidgetState widgetState) {
if (!PlayerWidget.isEnabled(context) || widgetState == null) {
return;
}
return n - 1;
}
private void updateViews() {
ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
ComponentName playerWidget = new ComponentName(context, PlayerWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(context);
int[] widgetIds = manager.getAppWidgetIds(playerWidget);
final PendingIntent startMediaPlayer = PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT);
final PendingIntent startMediaPlayer = PendingIntent.getActivity(context, R.id.pending_intent_player_activity,
PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews views;
views = new RemoteViews(getPackageName(), R.layout.player_widget);
views = new RemoteViews(context.getPackageName(), R.layout.player_widget);
Playable media;
PlayerStatus status;
if (playbackService != null) {
media = playbackService.getPlayable();
status = playbackService.getStatus();
} else {
media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
status = PlayerStatus.STOPPED;
}
if (media != null) {
if (widgetState.media != null) {
Bitmap icon;
int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer);
try {
icon = Glide.with(PlayerWidgetJobService.this)
icon = Glide.with(context)
.asBitmap()
.load(ImageResourceUtils.getImageLocation(media))
.load(ImageResourceUtils.getImageLocation(widgetState.media))
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.submit(iconSize, iconSize)
.get(500, TimeUnit.MILLISECONDS);
views.setImageViewBitmap(R.id.imgvCover, icon);
} catch (Throwable tr1) {
try {
icon = Glide.with(PlayerWidgetJobService.this)
icon = Glide.with(context)
.asBitmap()
.load(ImageResourceUtils.getFallbackImageLocation(media))
.load(ImageResourceUtils.getFallbackImageLocation(widgetState.media))
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.submit(iconSize, iconSize)
.get(500, TimeUnit.MILLISECONDS);
@ -146,50 +99,44 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
}
}
views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
views.setTextViewText(R.id.txtvTitle, widgetState.media.getEpisodeTitle());
views.setViewVisibility(R.id.txtvTitle, View.VISIBLE);
views.setViewVisibility(R.id.txtNoPlaying, View.GONE);
String progressString;
if (playbackService != null) {
progressString = getProgressString(playbackService.getCurrentPosition(),
playbackService.getDuration(), playbackService.getCurrentPlaybackSpeed());
} else {
progressString = getProgressString(media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media));
}
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 (status == PlayerStatus.PLAYING) {
if (widgetState.status == PlayerStatus.PLAYING) {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp);
views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
views.setContentDescription(R.id.butPlay, context.getString(R.string.pause_label));
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_pause_white_48dp);
views.setContentDescription(R.id.butPlayExtended, getString(R.string.pause_label));
views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.pause_label));
} else {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
views.setContentDescription(R.id.butPlay, context.getString(R.string.play_label));
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp);
views.setContentDescription(R.id.butPlayExtended, getString(R.string.play_label));
views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label));
}
views.setOnClickPendingIntent(R.id.butPlay,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
views.setOnClickPendingIntent(R.id.butPlayExtended,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
views.setOnClickPendingIntent(R.id.butRew,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_REWIND));
createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND));
views.setOnClickPendingIntent(R.id.butFastForward,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD));
createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD));
views.setOnClickPendingIntent(R.id.butSkip,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_NEXT));
createMediaButtonIntent(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,
createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
createMediaButtonIntent(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);
@ -201,7 +148,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
for (int id : widgetIds) {
Bundle options = manager.getAppWidgetOptions(id);
SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
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) {
@ -232,18 +179,32 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
}
/**
* Creates an intent which fakes a mediabutton press
* Returns number of cells needed for given size of the widget.
*
* @param size Widget size in dp.
* @return Size in number of cells.
*/
private PendingIntent createMediaButtonIntent(int eventCode) {
private static int getCellsForSize(int size) {
int n = 2;
while (70 * n - 30 < size) {
++n;
}
return n - 1;
}
/**
* Creates an intent which fakes a mediabutton press.
*/
private static PendingIntent createMediaButtonIntent(Context context, int eventCode) {
KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, eventCode);
Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class);
Intent startingIntent = new Intent(context, MediaButtonReceiver.class);
startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
return PendingIntent.getBroadcast(this, eventCode, startingIntent, 0);
return PendingIntent.getBroadcast(context, eventCode, startingIntent, 0);
}
private String getProgressString(int position, int duration, float speed) {
private static String getProgressString(int position, int duration, float speed) {
if (position >= 0 && duration > 0) {
TimeSpeedConverter converter = new TimeSpeedConverter(speed);
position = converter.convert(position);
@ -254,24 +215,4 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
return null;
}
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "Connection to service established");
if (service instanceof PlaybackService.LocalBinder) {
synchronized (waitForService) {
playbackService = ((PlaybackService.LocalBinder) service).getService();
waitForService.notifyAll();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (waitUsingService) {
playbackService = null;
}
Log.d(TAG, "Disconnected from service");
}
};
}

View File

@ -0,0 +1,32 @@
package de.danoeh.antennapod.core.widget;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.core.app.SafeJobIntentService;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.playback.Playable;
public class WidgetUpdaterJobService extends SafeJobIntentService {
private static final int JOB_ID = -17001;
/**
* Loads the current media from the database and updates the widget in a background job.
*/
public static void performBackgroundUpdate(Context context) {
enqueueWork(context, WidgetUpdaterJobService.class,
WidgetUpdaterJobService.JOB_ID, new Intent(context, WidgetUpdaterJobService.class));
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
Playable media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
if (media != null) {
WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED,
media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)));
} else {
WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(PlayerStatus.STOPPED));
}
}
}