Skip to content

Commit a928247

Browse files
committed
fixed playback controller bug
1 parent 1e6709b commit a928247

File tree

4 files changed

+159
-138
lines changed

4 files changed

+159
-138
lines changed

app/src/main/java/com/hhst/youtubelite/PlaybackService.java

Lines changed: 150 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,27 @@
2626
import com.hhst.youtubelite.webview.YoutubeWebview;
2727

2828
import java.io.IOException;
29+
import java.io.InputStream;
2930
import java.net.HttpURLConnection;
3031
import java.net.URL;
32+
import java.util.concurrent.ExecutorService;
3133
import java.util.concurrent.Executors;
3234

33-
3435
public class PlaybackService extends Service {
3536

36-
private MediaSessionCompat mediaSession;
37+
private static final String TAG = "PlaybackService";
3738
private static final String CHANNEL_ID = "player_channel";
3839
private static final int NOTIFICATION_ID = 100;
39-
private NotificationManager notificationManager;
4040

41-
@Override
42-
public int onStartCommand(Intent intent, int flags, int startId) {
43-
MediaButtonReceiver.handleIntent(mediaSession, intent);
44-
return super.onStartCommand(intent, flags, startId);
45-
}
41+
private MediaSessionCompat mediaSession;
42+
private NotificationManager notificationManager;
43+
private final Handler handler = new Handler(Looper.getMainLooper());
44+
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
4645

4746
public class PlaybackBinder extends Binder {
4847
public PlaybackService getService() {
4948
return PlaybackService.this;
5049
}
51-
5250
}
5351

5452
@Nullable
@@ -57,210 +55,228 @@ public IBinder onBind(Intent intent) {
5755
return new PlaybackBinder();
5856
}
5957

58+
@Override
59+
public void onCreate() {
60+
super.onCreate();
61+
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
62+
63+
NotificationChannel channel = new NotificationChannel(
64+
CHANNEL_ID,
65+
"Player Controls",
66+
NotificationManager.IMPORTANCE_LOW);
67+
channel.setDescription("Media playback controls");
68+
channel.setShowBadge(false);
69+
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
70+
notificationManager.createNotificationChannel(channel);
71+
72+
mediaSession = new MediaSessionCompat(this, TAG);
73+
PlaybackStateCompat initialState = new PlaybackStateCompat.Builder()
74+
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE |
75+
PlaybackStateCompat.ACTION_PLAY_PAUSE |
76+
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
77+
PlaybackStateCompat.ACTION_SEEK_TO)
78+
.setState(PlaybackStateCompat.STATE_NONE, 0, 1.0f)
79+
.build();
80+
mediaSession.setPlaybackState(initialState);
81+
}
82+
83+
@Override
84+
public int onStartCommand(Intent intent, int flags, int startId) {
85+
MediaButtonReceiver.handleIntent(mediaSession, intent);
86+
return super.onStartCommand(intent, flags, startId);
87+
}
88+
89+
private boolean isSeeking = false;
90+
private final Runnable resetSeekFlagRunnable = () -> isSeeking = false;
6091
public void initialize(YoutubeWebview webview) {
92+
if (webview == null) {
93+
stopSelf();
94+
return;
95+
}
96+
6197
mediaSession.setCallback(new MediaSessionCompat.Callback() {
6298
@Override
6399
public void onPlay() {
64-
super.onPlay();
65100
webview.evaluateJavascript("window.dispatchEvent(new Event('play'));", null);
66-
67-
// --- Update Notification ---
68-
Notification updatedNotification = buildNotification(true);
69-
if (updatedNotification != null && notificationManager != null) {
70-
notificationManager.notify(NOTIFICATION_ID, updatedNotification);
71-
}
72-
// --- End Update ---
73101
}
74102

75103
@Override
76104
public void onPause() {
77-
super.onPause();
78105
webview.evaluateJavascript("window.dispatchEvent(new Event('pause'));", null);
79-
80-
// --- Update Notification ---
81-
Notification updatedNotification = buildNotification(false);
82-
if (updatedNotification != null && notificationManager != null) {
83-
notificationManager.notify(NOTIFICATION_ID, updatedNotification);
84-
}
85-
// --- End Update ---
86106
}
87107

88108
@Override
89109
public void onSkipToNext() {
90-
super.onSkipToNext();
91110
webview.evaluateJavascript("window.dispatchEvent(new Event('skipToNext'));", null);
92111
}
93112

94113
@Override
95114
public void onSkipToPrevious() {
96-
super.onSkipToPrevious();
97115
webview.evaluateJavascript("window.dispatchEvent(new Event('skipToPrevious'));", null);
98116
}
99117

100118
@SuppressLint("DefaultLocale")
101119
@Override
102120
public void onSeekTo(long pos) {
103-
super.onSeekTo(pos);
121+
isSeeking = true;
122+
handler.removeCallbacks(resetSeekFlagRunnable);
123+
handler.postDelayed(resetSeekFlagRunnable, 1000);
124+
125+
long seekSeconds = Math.round(pos / 1000f);
104126
webview.evaluateJavascript(String.format(
105127
"window.dispatchEvent(new CustomEvent('seek', { detail: { time: %d } }));",
106-
Math.round(pos / 1000f)
128+
seekSeconds
107129
), null);
108130
}
109131
});
110132
mediaSession.setActive(true);
111133
}
112134

113-
114135
private Bitmap fetchThumbnail(String url) {
136+
if (url == null || url.isEmpty()) return null;
137+
Bitmap bitmap = null;
138+
HttpURLConnection conn = null;
139+
InputStream inputStream = null;
115140
try {
116-
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
141+
conn = (HttpURLConnection) new URL(url).openConnection();
117142
conn.setConnectTimeout(5000);
118-
Bitmap original = BitmapFactory.decodeStream(conn.getInputStream());
119-
120-
// centered clip
121-
int width = original.getWidth();
122-
int height = original.getHeight();
123-
124-
int size = Math.min(width, height);
125-
126-
int x = (width - size) / 2;
127-
int y = (height - size) / 2;
128-
129-
return Bitmap.createBitmap(original, x, y, size, size);
143+
conn.setReadTimeout(10000);
144+
conn.connect();
145+
int responseCode = conn.getResponseCode();
146+
if (responseCode == HttpURLConnection.HTTP_OK) {
147+
inputStream = conn.getInputStream();
148+
Bitmap original = BitmapFactory.decodeStream(inputStream);
149+
if (original != null) {
150+
int size = Math.min(original.getWidth(), original.getHeight());
151+
int x = (original.getWidth() - size) / 2;
152+
int y = (original.getHeight() - size) / 2;
153+
bitmap = Bitmap.createBitmap(original, x, y, size, size);
154+
if (bitmap != original) original.recycle();
155+
}
156+
}
130157
} catch (IOException e) {
131-
Log.e("fetch thumbnail error", Log.getStackTraceString(e));
132-
return null;
158+
Log.e(TAG, "fetchThumbnail IOException: " + e.getMessage());
159+
} finally {
160+
if (inputStream != null) {
161+
try { inputStream.close(); } catch (IOException ignored) {}
162+
}
163+
if (conn != null) conn.disconnect();
133164
}
165+
return bitmap;
134166
}
135167

136-
private final Handler handler = new Handler(Looper.getMainLooper());
137-
138-
private long lastProgressPos = 0L;
139-
private final Runnable timeoutRunnable = () -> updateProgress(lastProgressPos, 1f, false);
140-
141-
public void updateProgress(long pos, float playbackSpeed, boolean isPlaying) {
142-
handler.removeCallbacks(timeoutRunnable);
143-
handler.postDelayed(timeoutRunnable, 1000);
144-
lastProgressPos = pos;
145-
var state = isPlaying ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
146-
PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
147-
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE |
148-
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
149-
PlaybackStateCompat.ACTION_SEEK_TO)
150-
.setState(state, pos, playbackSpeed)
151-
.build();
152-
mediaSession.setPlaybackState(playbackState);
153-
}
154-
155-
// Helper method to build the notification based on current state
156168
private Notification buildNotification(boolean isPlaying) {
157-
// 1. Get current metadata from MediaSession
158169
MediaMetadataCompat metadata = mediaSession.getController().getMetadata();
159-
if (metadata == null) {
160-
// Handle case where metadata isn't available yet
161-
Log.w("PlaybackService", "Cannot build notification: Metadata is null");
162-
return null;
163-
}
170+
if (metadata == null) return null;
164171

165-
Bitmap largeIcon = metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
166172
String title = metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
167173
String artist = metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
174+
Bitmap largeIcon = metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
168175

169-
// 2. Choose icon and title based on isPlaying
170176
int playPauseIconResId = isPlaying ? R.drawable.ic_pause : R.drawable.ic_play;
171-
String playPauseActionTitle = isPlaying ? "Pause" : "Play";
177+
String playPauseActionTitle = isPlaying ? getString(R.string.action_pause) : getString(R.string.action_play);
172178

173-
// 3. Rebuild PendingIntent for app launch
174-
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
175-
if (intent == null) {
176-
intent = new Intent(this, MainActivity.class);
177-
}
178-
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
179-
PendingIntent pendingIntent = PendingIntent.getActivity(
180-
this, 101, intent,
181-
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
182-
);
183-
184-
// 4. Build the notification
185-
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
179+
PendingIntent playPauseActionIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE);
180+
PendingIntent prevActionIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
181+
PendingIntent nextActionIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
182+
183+
Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
184+
if (launchIntent == null) launchIntent = new Intent(this, MainActivity.class);
185+
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
186+
PendingIntent contentIntent = PendingIntent.getActivity(this, 101, launchIntent,
187+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
188+
189+
return new NotificationCompat.Builder(this, CHANNEL_ID)
186190
.setSmallIcon(R.drawable.ic_launcher_foreground)
187191
.setContentTitle(title)
188192
.setContentText(artist)
189193
.setLargeIcon(largeIcon)
190-
.setContentIntent(pendingIntent)
191-
.setOngoing(isPlaying) // Keep notification when playing
192-
// --- Actions ---
193-
.addAction(R.drawable.ic_previous, "Previous", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS))
194-
.addAction(playPauseIconResId, playPauseActionTitle, MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)) // Dynamic Icon
195-
.addAction(R.drawable.ic_next, "Next", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT))
196-
// --- Media Style ---
194+
.setContentIntent(contentIntent)
195+
.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_STOP))
196+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
197+
.setOngoing(isPlaying)
198+
.addAction(R.drawable.ic_previous, getString(R.string.action_previous), prevActionIntent)
199+
.addAction(playPauseIconResId, playPauseActionTitle, playPauseActionIntent)
200+
.addAction(R.drawable.ic_next, getString(R.string.action_next), nextActionIntent)
197201
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
198202
.setMediaSession(mediaSession.getSessionToken())
199-
.setShowActionsInCompactView(0, 1, 2));
200-
201-
return builder.build();
203+
.setShowActionsInCompactView(0, 1, 2))
204+
.build();
202205
}
203206

204207
public void showNotification(String title, String author, String thumbnail, long duration) {
205-
Executors.newSingleThreadExecutor().execute(() -> {
208+
executorService.execute(() -> {
206209
Bitmap largeIcon = fetchThumbnail(thumbnail);
207-
208-
// Build initial metadata
209210
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
210-
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, largeIcon)
211211
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
212212
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, author)
213+
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, largeIcon)
213214
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration * 1000)
214215
.build();
215-
mediaSession.setMetadata(metadata); // Set metadata FIRST
216+
mediaSession.setMetadata(metadata);
216217

217-
// Build initial playback state
218218
PlaybackStateCompat initialState = new PlaybackStateCompat.Builder()
219219
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE |
220+
PlaybackStateCompat.ACTION_PLAY_PAUSE |
220221
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
221222
PlaybackStateCompat.ACTION_SEEK_TO)
222-
.setState(PlaybackStateCompat.STATE_PAUSED, 0L, 1f)
223+
.setState(PlaybackStateCompat.STATE_PAUSED, 0L, 1.0f)
223224
.build();
224-
mediaSession.setPlaybackState(initialState); // Set the state in the session
225-
226-
// Build the initial notification
227-
Notification initialNotification = buildNotification(true);
228-
229-
if (initialNotification != null) {
230-
// Start foreground service with the initial notification
231-
startForeground(NOTIFICATION_ID, initialNotification);
232-
} else {
233-
Log.e("PlaybackService", "Failed to create initial notification.");
225+
mediaSession.setPlaybackState(initialState);
226+
227+
Notification notification = buildNotification(false);
228+
if (notification != null) {
229+
try {
230+
startForeground(NOTIFICATION_ID, notification);
231+
} catch (Exception e) {
232+
Log.e(TAG, "startForeground failed: " + e.getMessage());
233+
}
234234
}
235235
});
236236
}
237237

238-
public void hideNotification() {
239-
stopForeground(true);
240-
notificationManager.cancelAll();
241-
}
242238

243-
@Override
244-
public void onCreate() {
245-
super.onCreate();
246-
mediaSession = new MediaSessionCompat(this, "MediaSession");
247239

248-
// create notification channel
249-
var channel = new NotificationChannel(
250-
CHANNEL_ID,
251-
"Player Channel",
252-
NotificationManager.IMPORTANCE_LOW);
253-
channel.setDescription("Channel for player controller notifications");
254-
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
255-
notificationManager.createNotificationChannel(channel);
256-
}
240+
private boolean lastIsPlayingState = false;
241+
private long lastProgressPos = 0L;
242+
private final Runnable timeoutRunnable = () -> updateProgress(lastProgressPos, 1f, false);
243+
244+
public void updateProgress(long pos, float playbackSpeed, boolean isPlaying) {
245+
if (isSeeking) return;
246+
247+
handler.removeCallbacks(timeoutRunnable);
248+
handler.postDelayed(timeoutRunnable, 1000);
249+
lastProgressPos = pos;
257250

251+
int stateCompat = isPlaying ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
252+
PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
253+
.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE |
254+
PlaybackStateCompat.ACTION_PLAY_PAUSE |
255+
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
256+
PlaybackStateCompat.ACTION_SEEK_TO)
257+
.setState(stateCompat, pos, playbackSpeed)
258+
.build();
259+
260+
mediaSession.setPlaybackState(playbackState);
261+
if (isPlaying != lastIsPlayingState) {
262+
Notification updatedNotification = buildNotification(isPlaying);
263+
if (updatedNotification != null) {
264+
notificationManager.notify(NOTIFICATION_ID, updatedNotification);
265+
}
266+
}
267+
lastIsPlayingState = isPlaying;
268+
}
258269

259270
@Override
260271
public void onDestroy() {
261272
super.onDestroy();
262-
stopForeground(true);
263-
notificationManager.cancelAll();
264-
mediaSession.release();
273+
stopForeground(Service.STOP_FOREGROUND_REMOVE);
274+
if (notificationManager != null) notificationManager.cancelAll();
275+
if (mediaSession != null) {
276+
mediaSession.release();
277+
mediaSession = null;
278+
}
279+
handler.removeCallbacksAndMessages(null);
280+
executorService.shutdown();
265281
}
266282
}

0 commit comments

Comments
 (0)