diff --git a/.gitignore b/.gitignore index 2b3c40d66..a68965416 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.idea /*.iml gradle.properties +*~ diff --git a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/AudioStream.java b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/AudioStream.java index c345b3fa8..98eb17620 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/AudioStream.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/AudioStream.java @@ -31,14 +31,14 @@ public class AudioStream { this.bandwidth = bandwidth; this.sampling_rate = samplingRate; } - // reveals wether two streams are the same, but have diferent urls + // reveals whether two streams are the same, but have different urls public boolean equalStats(AudioStream cmp) { return format == cmp.format && bandwidth == cmp.bandwidth && sampling_rate == cmp.sampling_rate; } - // revelas wether two streams are equal + // reveals whether two streams are equal public boolean equals(AudioStream cmp) { return cmp != null && equalStats(cmp) && url == cmp.url; diff --git a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamExtractor.java index 66c500358..0f4eb7502 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamExtractor.java @@ -37,14 +37,14 @@ public abstract class StreamExtractor { private UrlIdHandler urlIdHandler; private StreamPreviewInfoCollector previewInfoCollector; - public class ExctractorInitException extends ExtractionException { - public ExctractorInitException(String message) { + public class ExtractorInitException extends ExtractionException { + public ExtractorInitException(String message) { super(message); } - public ExctractorInitException(Throwable cause) { + public ExtractorInitException(Throwable cause) { super(cause); } - public ExctractorInitException(String message, Throwable cause) { + public ExtractorInitException(String message, Throwable cause) { super(message, cause); } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfo.java index 23b90b083..5a1c0b740 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfo.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/stream_info/StreamInfo.java @@ -86,8 +86,8 @@ public class StreamInfo extends AbstractStreamInfo { private static StreamInfo extractImportantData( StreamInfo streamInfo, StreamExtractor extractor) throws ExtractionException, IOException { - /* ---- importand data, withoug the video can't be displayed goes here: ---- */ - // if one of these is not available an exception is ment to be thrown directly into the frontend. + /* ---- important data, withoug the video can't be displayed goes here: ---- */ + // if one of these is not available an exception is meant to be thrown directly into the frontend. UrlIdHandler uiconv = extractor.getUrlIdHandler(); @@ -134,7 +134,7 @@ public class StreamInfo extends AbstractStreamInfo { streamInfo.audio_streams = new Vector<>(); } //todo: make this quick and dirty solution a real fallback - // same as the quick and dirty aboth + // same as the quick and dirty above try { streamInfo.audio_streams.addAll( DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl)); @@ -173,9 +173,9 @@ public class StreamInfo extends AbstractStreamInfo { private static StreamInfo extractOptionalData( StreamInfo streamInfo, StreamExtractor extractor) { /* ---- optional data goes here: ---- */ - // If one of these failes, the frontend neets to handle that they are not available. - // Exceptions are therfore not thrown into the frontend, but stored into the error List, - // so the frontend can afterwads check where errors happend. + // If one of these fails, the frontend needs to handle that they are not available. + // Exceptions are therefore not thrown into the frontend, but stored into the error List, + // so the frontend can afterwards check where errors happened. try { streamInfo.thumbnail_url = extractor.getThumbnailUrl(); @@ -290,4 +290,4 @@ public class StreamInfo extends AbstractStreamInfo { public int start_position = 0; public List errors = new Vector<>(); -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 8ec525e77..eba1d57f2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -14,6 +14,8 @@ import android.media.AudioManager; import android.media.MediaPlayer; import android.net.wifi.WifiManager; import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; import android.os.PowerManager; import android.support.v7.app.NotificationCompat; import android.util.Log; @@ -27,6 +29,7 @@ import org.schabi.newpipe.detail.VideoItemDetailActivity; import org.schabi.newpipe.detail.VideoItemDetailFragment; import java.io.IOException; +import java.util.Arrays; /** * Created by Adam Howard on 08/11/15. @@ -51,10 +54,13 @@ import java.io.IOException; /**Plays the audio stream of videos in the background.*/ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ { - private static final String TAG = BackgroundPlayer.class.toString(); - private static final String ACTION_STOP = TAG + ".STOP"; - private static final String ACTION_PLAYPAUSE = TAG + ".PLAYPAUSE"; - private static final String ACTION_REWIND = TAG + ".REWIND"; + private static final String TAG = "BackgroundPlayer"; + private static final String CLASSNAME = "org.schabi.newpipe.player.BackgroundPlayer"; + private static final String ACTION_STOP = CLASSNAME + ".STOP"; + private static final String ACTION_PLAYPAUSE = CLASSNAME + ".PLAYPAUSE"; + private static final String ACTION_REWIND = CLASSNAME + ".REWIND"; + private static final String ACTION_PLAYBACK_STATE = CLASSNAME + ".PLAYBACK_STATE"; + private static final String EXTRA_PLAYBACK_STATE = CLASSNAME + ".extras.EXTRA_PLAYBACK_STATE"; // Extra intent arguments public static final String TITLE = "title"; @@ -114,6 +120,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare isRunning = false; } + private class PlayerThread extends Thread { MediaPlayer mediaPlayer; private String source; @@ -123,8 +130,8 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare private NotificationManager noteMgr; private WifiManager.WifiLock wifiLock; private Bitmap videoThumbnail; - private NotificationCompat.Builder noteBuilder; - private Notification note; + private NoteBuilder noteBuilder; + private volatile boolean donePlaying = false; public PlayerThread(String src, String title, BackgroundPlayer owner) { this.source = src; @@ -134,6 +141,45 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); } + public boolean isDonePlaying() { + return donePlaying; + } + + private boolean isPlaying() { + try { + return mediaPlayer.isPlaying(); + } catch (IllegalStateException e) { + Log.w(TAG, "Unable to retrieve playing state", e); + return false; + } + } + + private void setDonePlaying() { + donePlaying = true; + synchronized (PlayerThread.this) { + PlayerThread.this.notifyAll(); + } + } + + private synchronized PlaybackState getPlaybackState() { + try { + return new PlaybackState(mediaPlayer.getDuration(), mediaPlayer.getCurrentPosition(), isPlaying()); + } catch (IllegalStateException e) { + // This isn't that nice way to handle this. + // maybe there is a better way + Log.w(TAG, this + ": Got illegal state exception while creating playback state", e); + return PlaybackState.UNPREPARED; + } + } + + private void broadcastState() { + PlaybackState state = getPlaybackState(); + if(state == null) return; + Intent intent = new Intent(ACTION_PLAYBACK_STATE); + intent.putExtra(EXTRA_PLAYBACK_STATE, state); + sendBroadcast(intent); + } + @Override public void run() { mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock @@ -181,27 +227,29 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare filter.addAction(ACTION_PLAYPAUSE); filter.addAction(ACTION_STOP); filter.addAction(ACTION_REWIND); + filter.addAction(ACTION_PLAYBACK_STATE); registerReceiver(broadcastReceiver, filter); - note = buildNotification(); - - startForeground(noteID, note); + initNotificationBuilder(); + startForeground(noteID, noteBuilder.build()); //currently decommissioned progressbar looping update code - works, but doesn't fit inside //Notification.MediaStyle Notification layout. noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - /* + //update every 2s or 4 times in the video, whichever is shorter - int sleepTime = Math.min(2000, (int)((double)vidLength/4)); - while(mediaPlayer.isPlaying()) { - noteBuilder.setProgress(vidLength, mediaPlayer.getCurrentPosition(), false); - noteMgr.notify(noteID, noteBuilder.build()); + int vidLength = mediaPlayer.getDuration(); + int sleepTime = Math.min(2000, (int)(vidLength / 4)); + while(!isDonePlaying()) { + broadcastState(); try { - Thread.sleep(sleepTime); + synchronized (this) { + wait(sleepTime); + } } catch (InterruptedException e) { - Log.d(TAG, "sleep failure"); + Log.e(TAG, "sleep failure", e); } - }*/ + } } /**Handles button presses from the notification. */ @@ -210,39 +258,50 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare public void onReceive(Context context, Intent intent) { String action = intent.getAction(); //Log.i(TAG, "received broadcast action:"+action); - if(action.equals(ACTION_PLAYPAUSE)) { - if(mediaPlayer.isPlaying()) { - mediaPlayer.pause(); - note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); - if(android.os.Build.VERSION.SDK_INT >=16){ - note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); + switch (action) { + case ACTION_PLAYPAUSE: { + boolean isPlaying = mediaPlayer.isPlaying(); + if(isPlaying) { + mediaPlayer.pause(); + } else { + //reacquire CPU lock after auto-releasing it on pause + mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + mediaPlayer.start(); } - noteMgr.notify(noteID, note); - } - else { - //reacquire CPU lock after auto-releasing it on pause - mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); - mediaPlayer.start(); - note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); - if(android.os.Build.VERSION.SDK_INT >=16){ - note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); + synchronized (PlayerThread.this) { + PlayerThread.this.notifyAll(); } - noteMgr.notify(noteID, note); + break; + } + case ACTION_REWIND: + mediaPlayer.seekTo(0); + synchronized (PlayerThread.this) { + PlayerThread.this.notifyAll(); + } + break; + case ACTION_STOP: + //this auto-releases CPU lock + mediaPlayer.stop(); + afterPlayCleanup(); + break; + case ACTION_PLAYBACK_STATE: { + PlaybackState playbackState = intent.getParcelableExtra(EXTRA_PLAYBACK_STATE); + if(!playbackState.equals(PlaybackState.UNPREPARED)) { + noteBuilder.setProgress(playbackState.getDuration(), playbackState.getPlayedTime(), false); + noteBuilder.setIsPlaying(playbackState.isPlaying()); + } else { + noteBuilder.setProgress(0, 0, true); + } + noteMgr.notify(noteID, noteBuilder.build()); + break; } - } - else if(action.equals(ACTION_REWIND)) { - mediaPlayer.seekTo(0); -// noteMgr.notify(noteID, note); - } - else if(action.equals(ACTION_STOP)) { - //this auto-releases CPU lock - mediaPlayer.stop(); - afterPlayCleanup(); } } }; private void afterPlayCleanup() { + // Notify thread to stop + setDonePlaying(); //remove progress bar //noteBuilder.setProgress(0, 0, false); @@ -256,7 +315,12 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare wifiLock.release(); //remove foreground status of service; make BackgroundPlayer killable stopForeground(true); - + try { + // Wait for thread to stop + PlayerThread.this.join(); + } catch (InterruptedException e) { + Log.e(TAG, "unable to join player thread", e); + } stopSelf(); } @@ -272,10 +336,14 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare } } - private Notification buildNotification() { + private void initNotificationBuilder() { Notification note; Resources res = getApplicationContext().getResources(); - noteBuilder = new NotificationCompat.Builder(owner); + + /* + NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder + (R.drawable.ic_pause_white_24dp, "Pause", playPI).build(); + */ PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT); @@ -283,10 +351,6 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent rewindPI = PendingIntent.getBroadcast(owner, noteID, new Intent(ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); - /* - NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder - (R.drawable.ic_pause_white_24dp, "Pause", playPI).build(); - */ //build intent to return to video, on tapping notification Intent openDetailViewIntent = new Intent(getApplicationContext(), @@ -296,58 +360,226 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID, openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT); - + noteBuilder = new NoteBuilder(owner, playPI, stopPI, rewindPI, openDetailView); noteBuilder + .setTitle(title) + .setArtist(channelName) .setOngoing(true) .setDeleteIntent(stopPI) //doesn't fit with Notification.MediaStyle //.setProgress(vidLength, 0, false) .setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp) - .setTicker( - String.format(res.getString( - R.string.background_player_time_text), title)) .setContentIntent(PendingIntent.getActivity(getApplicationContext(), noteID, openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT)) - .setContentIntent(openDetailView); + .setContentIntent(openDetailView) + .setCategory(Notification.CATEGORY_TRANSPORT) + //Make notification appear on lockscreen + .setVisibility(Notification.VISIBILITY_PUBLIC); + } - RemoteViews view = - new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); - view.setImageViewBitmap(R.id.notificationCover, videoThumbnail); - view.setTextViewText(R.id.notificationSongName, title); - view.setTextViewText(R.id.notificationArtist, channelName); - view.setOnClickPendingIntent(R.id.notificationStop, stopPI); - view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); - view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); - view.setOnClickPendingIntent(R.id.notificationContent, openDetailView); + /** + * Notification builder which works like the real builder but uses a custom view. + */ + class NoteBuilder extends NotificationCompat.Builder { - //possibly found the expandedView problem, - //but can't test it as I don't have a 5.0 device. -medavox - RemoteViews expandedView = - new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); - expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); - expandedView.setTextViewText(R.id.notificationSongName, title); - expandedView.setTextViewText(R.id.notificationArtist, channelName); - expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI); - expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); - expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); - expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView); - - - noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT); - - //Make notification appear on lockscreen - noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); - - note = noteBuilder.build(); - note.contentView = view; - - if (android.os.Build.VERSION.SDK_INT > 16) { - note.bigContentView = expandedView; + /** + * @param context + * @inheritDoc + */ + public NoteBuilder(Context context, PendingIntent playPI, PendingIntent stopPI, + PendingIntent rewindPI, PendingIntent openDetailView) { + super(context); + setCustomContentView(createCustomContentView(playPI, stopPI, rewindPI, openDetailView)); + setCustomBigContentView(createCustomBigContentView(playPI, stopPI, rewindPI, openDetailView)); } - return note; + private RemoteViews createCustomBigContentView(PendingIntent playPI, + PendingIntent stopPI, + PendingIntent rewindPI, + PendingIntent openDetailView) { + //possibly found the expandedView problem, + //but can't test it as I don't have a 5.0 device. -medavox + RemoteViews expandedView = + new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); + expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI); + expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); + expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); + expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView); + return expandedView; + } + + private RemoteViews createCustomContentView(PendingIntent playPI, PendingIntent stopPI, + PendingIntent rewindPI, + PendingIntent openDetailView) { + RemoteViews view = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); + view.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + view.setOnClickPendingIntent(R.id.notificationStop, stopPI); + view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); + view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); + view.setOnClickPendingIntent(R.id.notificationContent, openDetailView); + return view; + } + + /** + * Set the title of the stream + * @param title the title of the stream + * @return this builder for chaining + */ + NoteBuilder setTitle(String title) { + setContentTitle(title); + getContentView().setTextViewText(R.id.notificationSongName, title); + getBigContentView().setTextViewText(R.id.notificationSongName, title); + setTicker(String.format(getBaseContext().getString( + R.string.background_player_time_text), title)); + return this; + } + + /** + * Set the artist of the stream + * @param artist the artist of the stream + * @return this builder for chaining + */ + NoteBuilder setArtist(String artist) { + setSubText(artist); + getContentView().setTextViewText(R.id.notificationArtist, artist); + getBigContentView().setTextViewText(R.id.notificationArtist, artist); + return this; + } + + @Override + public android.support.v4.app.NotificationCompat.Builder setProgress(int max, int progress, boolean indeterminate) { + super.setProgress(max, progress, indeterminate); + getBigContentView().setProgressBar(R.id.playbackProgress, max, progress, indeterminate); + return this; + } + + /** + * Set the isPlaying state + * @param isPlaying the is playing state + */ + public void setIsPlaying(boolean isPlaying) { + RemoteViews views = getContentView(), bigViews = getBigContentView(); + int imageSrc; + if(isPlaying) { + imageSrc = R.drawable.ic_pause_white_24dp; + } else { + imageSrc = R.drawable.ic_play_circle_filled_white_24dp; + } + views.setImageViewResource(R.id.notificationPlayPause, imageSrc); + bigViews.setImageViewResource(R.id.notificationPlayPause, imageSrc); + + } + + } + } + + /** + * Represents the state of the player. + */ + public static class PlaybackState implements Parcelable { + + private static final int INDEX_IS_PLAYING = 0; + private static final int INDEX_IS_PREPARED= 1; + private static final int INDEX_HAS_ERROR = 2; + private final int duration; + private final int played; + private final boolean[] booleanValues = new boolean[3]; + + static final PlaybackState UNPREPARED = new PlaybackState(false, false, false); + static final PlaybackState FAILED = new PlaybackState(false, false, true); + + + PlaybackState(Parcel in) { + duration = in.readInt(); + played = in.readInt(); + in.readBooleanArray(booleanValues); + } + + PlaybackState(int duration, int played, boolean isPlaying) { + this.played = played; + this.duration = duration; + this.booleanValues[INDEX_IS_PLAYING] = isPlaying; + this.booleanValues[INDEX_IS_PREPARED] = true; + this.booleanValues[INDEX_HAS_ERROR] = false; + } + + private PlaybackState(boolean isPlaying, boolean isPrepared, boolean hasErrors) { + this.played = 0; + this.duration = 0; + this.booleanValues[INDEX_IS_PLAYING] = isPlaying; + this.booleanValues[INDEX_IS_PREPARED] = isPrepared; + this.booleanValues[INDEX_HAS_ERROR] = hasErrors; + } + + int getDuration() { + return duration; + } + + int getPlayedTime() { + return played; + } + + boolean isPlaying() { + return booleanValues[INDEX_IS_PLAYING]; + } + + boolean isPrepared() { + return booleanValues[INDEX_IS_PREPARED]; + } + + boolean hasErrors() { + return booleanValues[INDEX_HAS_ERROR]; + } + + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(duration); + dest.writeInt(played); + dest.writeBooleanArray(booleanValues); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public PlaybackState createFromParcel(Parcel in) { + return new PlaybackState(in); + } + + @Override + public PlaybackState[] newArray(int size) { + return new PlaybackState[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PlaybackState that = (PlaybackState) o; + + if (duration != that.duration) return false; + if (played != that.played) return false; + return Arrays.equals(booleanValues, that.booleanValues); + + } + + @Override + public int hashCode() { + if(this == UNPREPARED) return 1; + if(this == FAILED) return 2; + int result = duration; + result = 31 * result + played; + result = 31 * result + Arrays.hashCode(booleanValues); + return result + 2; } } } diff --git a/app/src/main/res/layout/player_notification_expanded.xml b/app/src/main/res/layout/player_notification_expanded.xml index 5a8b3a9d3..0fb6a8eb2 100644 --- a/app/src/main/res/layout/player_notification_expanded.xml +++ b/app/src/main/res/layout/player_notification_expanded.xml @@ -20,7 +20,7 @@ android:layout_above="@+id/notificationButtons" android:layout_toRightOf="@+id/notificationCover" android:gravity="center_vertical" - android:orientation="vertical" > + android:orientation="vertical"> + +