convert PlayerHolder to Singleton, handle context within, bugfix ServiceConnection leak

- bugfix: have ServiceConnection created only once!

- select the context within the PlayerHolder to start, stop, bind or unbind the service
  -> we have to make sure the Service is started AND stopped within the same context
  -> so let PlayerHolder be the one to select the context

- remove removeListener() and replace the call with setListener(null)
- Compatibility: use ContextCompat.startForegroundService instead of startService()
This commit is contained in:
evermind 2021-06-24 10:00:56 +02:00
parent 435813355f
commit 48c2c156cb
11 changed files with 98 additions and 76 deletions

View file

@ -823,7 +823,7 @@ public class MainActivity extends AppCompatActivity {
return; return;
} }
if (PlayerHolder.isPlayerOpen()) { if (PlayerHolder.getInstance().isPlayerOpen()) {
// if the player is already open, no need for a broadcast receiver // if the player is already open, no need for a broadcast receiver
openMiniPlayerIfMissing(); openMiniPlayerIfMissing();
} else { } else {

View file

@ -453,7 +453,7 @@ public class RouterActivity extends AppCompatActivity {
returnList.add(showInfo); returnList.add(showInfo);
returnList.add(videoPlayer); returnList.add(videoPlayer);
} else { } else {
final MainPlayer.PlayerType playerType = PlayerHolder.getType(); final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
if (capabilities.contains(VIDEO) if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context) && PlayerHelper.isAutoplayAllowedByUser(context)
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) { && playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {

View file

@ -201,6 +201,7 @@ public final class VideoDetailFragment
@Nullable @Nullable
private MainPlayer playerService; private MainPlayer playerService;
private Player player; private Player player;
private PlayerHolder playerHolder = PlayerHolder.getInstance();
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service management // Service management
@ -360,9 +361,9 @@ public final class VideoDetailFragment
// Stop the service when user leaves the app with double back press // Stop the service when user leaves the app with double back press
// if video player is selected. Otherwise unbind // if video player is selected. Otherwise unbind
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) { if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
PlayerHolder.stopService(App.getApp()); playerHolder.stopService();
} else { } else {
PlayerHolder.removeListener(); playerHolder.setListener(null);
} }
PreferenceManager.getDefaultSharedPreferences(activity) PreferenceManager.getDefaultSharedPreferences(activity)
@ -660,10 +661,10 @@ public final class VideoDetailFragment
}); });
setupBottomPlayer(); setupBottomPlayer();
if (!PlayerHolder.bound) { if (!playerHolder.bound) {
setHeightThumbnail(); setHeightThumbnail();
} else { } else {
PlayerHolder.startService(App.getApp(), false, this); playerHolder.startService(false, this);
} }
} }
@ -1097,7 +1098,7 @@ public final class VideoDetailFragment
// See UI changes while remote playQueue changes // See UI changes while remote playQueue changes
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
PlayerHolder.startService(App.getApp(), false, this); playerHolder.startService(false, this);
} }
toggleFullscreenIfInFullscreenMode(); toggleFullscreenIfInFullscreenMode();
@ -1123,7 +1124,7 @@ public final class VideoDetailFragment
private void openNormalBackgroundPlayer(final boolean append) { private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes // See UI changes while remote playQueue changes
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
PlayerHolder.startService(App.getApp(), false, this); playerHolder.startService(false, this);
} }
final PlayQueue queue = setupPlayQueueForIntent(append); final PlayQueue queue = setupPlayQueueForIntent(append);
@ -1137,7 +1138,7 @@ public final class VideoDetailFragment
private void openMainPlayer() { private void openMainPlayer() {
if (!isPlayerServiceAvailable()) { if (!isPlayerServiceAvailable()) {
PlayerHolder.startService(App.getApp(), autoPlayEnabled, this); playerHolder.startService(autoPlayEnabled, this);
return; return;
} }
if (currentInfo == null) { if (currentInfo == null) {
@ -1155,7 +1156,7 @@ public final class VideoDetailFragment
final Intent playerIntent = NavigationHelper final Intent playerIntent = NavigationHelper
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled); .getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
activity.startService(playerIntent); ContextCompat.startForegroundService(activity, playerIntent);
} }
private void hideMainPlayer() { private void hideMainPlayer() {
@ -1373,9 +1374,9 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} }
// Rebound to the service if it was closed via notification or mini player // Rebound to the service if it was closed via notification or mini player
if (!PlayerHolder.bound) { if (!playerHolder.bound) {
PlayerHolder.startService( playerHolder.startService(
App.getApp(), false, VideoDetailFragment.this); false, VideoDetailFragment.this);
} }
break; break;
} }
@ -2119,7 +2120,7 @@ public final class VideoDetailFragment
if (currentWorker != null) { if (currentWorker != null) {
currentWorker.dispose(); currentWorker.dispose();
} }
PlayerHolder.stopService(App.getApp()); playerHolder.stopService();
setInitialData(0, null, "", null); setInitialData(0, null, "", null);
currentInfo = null; currentInfo = null;
updateOverlayData(null, null, null); updateOverlayData(null, null, null);

View file

@ -353,7 +353,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
final List<StreamDialogEntry> entries = new ArrayList<>(); final List<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) { if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue); entries.add(StreamDialogEntry.enqueue);
} }
if (item.getStreamType() == StreamType.AUDIO_STREAM) { if (item.getStreamType() == StreamType.AUDIO_STREAM) {

View file

@ -144,7 +144,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
final ArrayList<StreamDialogEntry> entries = new ArrayList<>(); final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) { if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue); entries.add(StreamDialogEntry.enqueue);
} }
if (item.getStreamType() == StreamType.AUDIO_STREAM) { if (item.getStreamType() == StreamType.AUDIO_STREAM) {

View file

@ -331,7 +331,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (context == null || context.resources == null || activity == null) return if (context == null || context.resources == null || activity == null) return
val entries = ArrayList<StreamDialogEntry>() val entries = ArrayList<StreamDialogEntry>()
if (PlayerHolder.getType() != null) { if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue) entries.add(StreamDialogEntry.enqueue)
} }
if (item.streamType == StreamType.AUDIO_STREAM) { if (item.streamType == StreamType.AUDIO_STREAM) {

View file

@ -340,7 +340,7 @@ public class StatisticsPlaylistFragment
final ArrayList<StreamDialogEntry> entries = new ArrayList<>(); final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) { if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue); entries.add(StreamDialogEntry.enqueue);
} }
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {

View file

@ -749,7 +749,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final ArrayList<StreamDialogEntry> entries = new ArrayList<>(); final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) { if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue); entries.add(StreamDialogEntry.enqueue);
} }
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {

View file

@ -8,6 +8,7 @@ import android.os.IBinder;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
@ -22,18 +23,27 @@ import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
public final class PlayerHolder { public final class PlayerHolder {
private PlayerHolder() { private PlayerHolder() {
} }
private static final boolean DEBUG = MainActivity.DEBUG; private static PlayerHolder instance;
private static final String TAG = "PlayerHolder"; public static synchronized PlayerHolder getInstance() {
if (PlayerHolder.instance == null) {
PlayerHolder.instance = new PlayerHolder();
}
return PlayerHolder.instance;
}
private static PlayerServiceExtendedEventListener listener; private final boolean DEBUG = MainActivity.DEBUG;
private final String TAG = PlayerHolder.class.getSimpleName();
private static ServiceConnection serviceConnection; private PlayerServiceExtendedEventListener listener;
public static boolean bound;
private static MainPlayer playerService; private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private static Player player; public boolean bound;
private MainPlayer playerService;
private Player player;
/** /**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
@ -42,26 +52,31 @@ public final class PlayerHolder {
* @return Current PlayerType * @return Current PlayerType
*/ */
@Nullable @Nullable
public static MainPlayer.PlayerType getType() { public MainPlayer.PlayerType getType() {
if (player == null) { if (player == null) {
return null; return null;
} }
return player.getPlayerType(); return player.getPlayerType();
} }
public static boolean isPlaying() { public boolean isPlaying() {
if (player == null) { if (player == null) {
return false; return false;
} }
return player.isPlaying(); return player.isPlaying();
} }
public static boolean isPlayerOpen() { public boolean isPlayerOpen() {
return player != null; return player != null;
} }
public static void setListener(final PlayerServiceExtendedEventListener newListener) { public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
listener = newListener; listener = newListener;
if (listener == null) {
return;
}
// Force reload data from service // Force reload data from service
if (player != null) { if (player != null) {
listener.onServiceConnected(player, playerService, false); listener.onServiceConnected(player, playerService, false);
@ -69,14 +84,15 @@ public final class PlayerHolder {
} }
} }
public static void removeListener() { // helper to handle context in common place as using the same
listener = null; // context to bind/unbind a service is crucial
private Context getCommonContext() {
return App.getApp();
} }
public void startService(final boolean playAfterConnect,
public static void startService(final Context context, final PlayerServiceExtendedEventListener newListener) {
final boolean playAfterConnect, final Context context = getCommonContext();
final PlayerServiceExtendedEventListener newListener) {
setListener(newListener); setListener(newListener);
if (bound) { if (bound) {
return; return;
@ -85,58 +101,65 @@ public final class PlayerHolder {
// and NullPointerExceptions inside the service because the service will be // and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first // bound twice. Prevent it with unbinding first
unbind(context); unbind(context);
context.startService(new Intent(context, MainPlayer.class)); ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
serviceConnection = getServiceConnection(context, playAfterConnect); serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context); bind(context);
} }
public static void stopService(final Context context) { public void stopService() {
final Context context = getCommonContext();
unbind(context); unbind(context);
context.stopService(new Intent(context, MainPlayer.class)); context.stopService(new Intent(context, MainPlayer.class));
} }
private static ServiceConnection getServiceConnection(final Context context, class PlayerServiceConnection implements ServiceConnection {
final boolean playAfterConnect) {
return new ServiceConnection() {
@Override
public void onServiceDisconnected(final ComponentName compName) {
if (DEBUG) {
Log.d(TAG, "Player service is disconnected");
}
unbind(context); private boolean playAfterConnect = false;
public void doPlayAfterConnect(final boolean playAfterConnection) {
this.playAfterConnect = playAfterConnection;
}
@Override
public void onServiceDisconnected(final ComponentName compName) {
if (DEBUG) {
Log.d(TAG, "Player service is disconnected");
} }
@Override final Context context = getCommonContext();
public void onServiceConnected(final ComponentName compName, final IBinder service) { unbind(context);
if (DEBUG) { }
Log.d(TAG, "Player service is connected");
}
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
playerService = localBinder.getService(); @Override
player = localBinder.getPlayer(); public void onServiceConnected(final ComponentName compName, final IBinder service) {
if (listener != null) { if (DEBUG) {
listener.onServiceConnected(player, playerService, playAfterConnect); Log.d(TAG, "Player service is connected");
}
startPlayerListener();
} }
}; final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
}
private static void bind(final Context context) { playerService = localBinder.getService();
player = localBinder.getPlayer();
if (listener != null) {
listener.onServiceConnected(player, playerService, playAfterConnect);
}
startPlayerListener();
}
};
private void bind(final Context context) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "bind() called"); Log.d(TAG, "bind() called");
} }
final Intent serviceIntent = new Intent(context, MainPlayer.class); final Intent serviceIntent = new Intent(context, MainPlayer.class);
bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
if (!bound) { if (!bound) {
context.unbindService(serviceConnection); context.unbindService(serviceConnection);
} }
} }
private static void unbind(final Context context) { private void unbind(final Context context) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "unbind() called"); Log.d(TAG, "unbind() called");
} }
@ -153,21 +176,19 @@ public final class PlayerHolder {
} }
} }
private void startPlayerListener() {
private static void startPlayerListener() {
if (player != null) { if (player != null) {
player.setFragmentListener(INNER_LISTENER); player.setFragmentListener(internalListener);
} }
} }
private static void stopPlayerListener() { private void stopPlayerListener() {
if (player != null) { if (player != null) {
player.removeFragmentListener(INNER_LISTENER); player.removeFragmentListener(internalListener);
} }
} }
private final PlayerServiceEventListener internalListener =
private static final PlayerServiceEventListener INNER_LISTENER =
new PlayerServiceEventListener() { new PlayerServiceEventListener() {
@Override @Override
public void onFullscreenStateChanged(final boolean fullscreen) { public void onFullscreenStateChanged(final boolean fullscreen) {
@ -242,7 +263,7 @@ public final class PlayerHolder {
if (listener != null) { if (listener != null) {
listener.onServiceStopped(); listener.onServiceStopped();
} }
unbind(App.getApp()); unbind(getCommonContext());
} }
}; };
} }

View file

@ -349,13 +349,13 @@ public final class NavigationHelper {
final boolean switchingPlayers) { final boolean switchingPlayers) {
final boolean autoPlay; final boolean autoPlay;
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getType(); @Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
if (playerType == null) { if (playerType == null) {
// no player open // no player open
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else if (switchingPlayers) { } else if (switchingPlayers) {
// switching player to main player // switching player to main player
autoPlay = PlayerHolder.isPlaying(); // keep play/pause state autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
} else if (playerType == MainPlayer.PlayerType.VIDEO) { } else if (playerType == MainPlayer.PlayerType.VIDEO) {
// opening new stream while already playing in main player // opening new stream while already playing in main player
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);

View file

@ -39,7 +39,7 @@ public enum StreamDialogEntry {
* Info: Add this entry within showStreamDialog. * Info: Add this entry within showStreamDialog.
*/ */
enqueue(R.string.enqueue_stream, (fragment, item) -> { enqueue(R.string.enqueue_stream, (fragment, item) -> {
final MainPlayer.PlayerType type = PlayerHolder.getType(); final MainPlayer.PlayerType type = PlayerHolder.getInstance().getType();
if (type == AUDIO) { if (type == AUDIO) {
NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(), NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(),