mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 06:43:00 +00:00 
			
		
		
		
	fix readme conflict
This commit is contained in:
		| @@ -48,7 +48,13 @@ android { | |||||||
| } | } | ||||||
|  |  | ||||||
| ext { | ext { | ||||||
|     supportLibVersion = '27.0.2' |     supportLibVersion = '27.1.0' | ||||||
|  |     exoPlayerLibVersion = '2.7.1' | ||||||
|  |     roomDbLibVersion = '1.0.0' | ||||||
|  |     leakCanaryLibVersion = '1.5.4' | ||||||
|  |     okHttpLibVersion = '1.5.0' | ||||||
|  |     icepickLibVersion = '3.2.0' | ||||||
|  |     stethoLibVersion = '1.5.0' | ||||||
| } | } | ||||||
| dependencies { | dependencies { | ||||||
|     androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { |     androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { | ||||||
| @@ -73,27 +79,28 @@ dependencies { | |||||||
|     implementation 'de.hdodenhof:circleimageview:2.2.0' |     implementation 'de.hdodenhof:circleimageview:2.2.0' | ||||||
|     implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' |     implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' | ||||||
|     implementation 'com.nononsenseapps:filepicker:4.2.1' |     implementation 'com.nononsenseapps:filepicker:4.2.1' | ||||||
|     implementation 'com.google.android.exoplayer:exoplayer:2.7.0' |     implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion" | ||||||
|  |     implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion" | ||||||
|  |  | ||||||
|     debugImplementation 'com.facebook.stetho:stetho:1.5.0' |     debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion" | ||||||
|     debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' |     debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion" | ||||||
|     debugImplementation 'com.android.support:multidex:1.0.2' |     debugImplementation 'com.android.support:multidex:1.0.3' | ||||||
|  |  | ||||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.1.7' |     implementation 'io.reactivex.rxjava2:rxjava:2.1.10' | ||||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' |     implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' | ||||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' |     implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' | ||||||
|  |  | ||||||
|     implementation 'android.arch.persistence.room:runtime:1.0.0' |     implementation "android.arch.persistence.room:runtime:$roomDbLibVersion" | ||||||
|     implementation 'android.arch.persistence.room:rxjava2:1.0.0' |     implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion" | ||||||
|     annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' |     annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion" | ||||||
|  |  | ||||||
|     implementation 'frankiesardo:icepick:3.2.0' |     implementation "frankiesardo:icepick:$icepickLibVersion" | ||||||
|     annotationProcessor 'frankiesardo:icepick-processor:3.2.0' |     annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion" | ||||||
|  |  | ||||||
|     debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' |     debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion" | ||||||
|     betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' |     betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" | ||||||
|     releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' |     releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion" | ||||||
|  |  | ||||||
|     implementation 'com.squareup.okhttp3:okhttp:3.9.1' |     implementation 'com.squareup.okhttp3:okhttp:3.9.1' | ||||||
|     debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' |     debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,6 +28,12 @@ | |||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|  |  | ||||||
|  |         <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" > | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.MEDIA_BUTTON" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </receiver> | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".player.old.PlayVideoActivity" |             android:name=".player.old.PlayVideoActivity" | ||||||
|             android:configChanges="orientation|keyboardHidden|screenSize" |             android:configChanges="orientation|keyboardHidden|screenSize" | ||||||
| @@ -43,12 +49,6 @@ | |||||||
|             android:launchMode="singleTask" |             android:launchMode="singleTask" | ||||||
|             android:label="@string/title_activity_background_player"/> |             android:label="@string/title_activity_background_player"/> | ||||||
|  |  | ||||||
|         <receiver android:name="org.schabi.newpipe.player.BackgroundPlayer$MediaButtonReceiver"> |  | ||||||
|             <intent-filter> |  | ||||||
|                 <action android:name="android.intent.action.MEDIA_BUTTON" /> |  | ||||||
|             </intent-filter> |  | ||||||
|         </receiver> |  | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".player.PopupVideoPlayerActivity" |             android:name=".player.PopupVideoPlayerActivity" | ||||||
|             android:launchMode="singleTask" |             android:launchMode="singleTask" | ||||||
|   | |||||||
| @@ -8,9 +8,7 @@ import android.support.v7.app.AppCompatActivity; | |||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.View; | import android.view.View; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; |  | ||||||
| import com.squareup.leakcanary.RefWatcher; | import com.squareup.leakcanary.RefWatcher; | ||||||
|  |  | ||||||
| import icepick.Icepick; | import icepick.Icepick; | ||||||
| @@ -94,35 +92,4 @@ public abstract class BaseFragment extends Fragment { | |||||||
|             activity.getSupportActionBar().setTitle(title); |             activity.getSupportActionBar().setTitle(title); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // DisplayImageOptions default configurations |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     public static final DisplayImageOptions BASE_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder().cacheInMemory(true).build(); |  | ||||||
|  |  | ||||||
|     public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_OPTIONS) |  | ||||||
|                     .showImageOnLoading(R.drawable.buddy) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.buddy) |  | ||||||
|                     .showImageOnFail(R.drawable.buddy) |  | ||||||
|                     .build(); |  | ||||||
|  |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_OPTIONS) |  | ||||||
|                     .displayer(new FadeInBitmapDisplayer(250)) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) |  | ||||||
|                     .build(); |  | ||||||
|  |  | ||||||
|     public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_OPTIONS) |  | ||||||
|                     .showImageOnLoading(R.drawable.channel_banner) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.channel_banner) |  | ||||||
|                     .showImageOnFail(R.drawable.channel_banner) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
| package org.schabi.newpipe; | package org.schabi.newpipe; | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.download.BaseImageDownloader; | import com.nostra13.universalimageloader.core.download.BaseImageDownloader; | ||||||
|  |  | ||||||
| @@ -10,16 +14,33 @@ import java.io.IOException; | |||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
|  |  | ||||||
| public class ImageDownloader extends BaseImageDownloader { | public class ImageDownloader extends BaseImageDownloader { | ||||||
|  |     private final Resources resources; | ||||||
|  |     private final SharedPreferences preferences; | ||||||
|  |     private final String downloadThumbnailKey; | ||||||
|  |  | ||||||
|     public ImageDownloader(Context context) { |     public ImageDownloader(Context context) { | ||||||
|         super(context); |         super(context); | ||||||
|  |         this.resources = context.getResources(); | ||||||
|  |         this.preferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |         this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public ImageDownloader(Context context, int connectTimeout, int readTimeout) { |     private boolean isDownloadingThumbnail() { | ||||||
|         super(context, connectTimeout, readTimeout); |         return preferences.getBoolean(downloadThumbnailKey, true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("ResourceType") | ||||||
|  |     @Override | ||||||
|  |     public InputStream getStream(String imageUri, Object extra) throws IOException { | ||||||
|  |         if (isDownloadingThumbnail()) { | ||||||
|  |             return super.getStream(imageUri, extra); | ||||||
|  |         } else { | ||||||
|  |             return resources.openRawResource(R.drawable.dummy_thumbnail_dark); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { |     protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { | ||||||
|         Downloader downloader = (Downloader) NewPipe.getDownloader(); |         final Downloader downloader = (Downloader) NewPipe.getDownloader(); | ||||||
|         return downloader.stream(imageUri); |         return downloader.stream(imageUri); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ import android.widget.Toast; | |||||||
|  |  | ||||||
| import com.nirhart.parallaxscroll.views.ParallaxScrollView; | import com.nirhart.parallaxscroll.views.ParallaxScrollView; | ||||||
| import com.nostra13.universalimageloader.core.assist.FailReason; | import com.nostra13.universalimageloader.core.assist.FailReason; | ||||||
|  | import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | ||||||
| import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; | import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| @@ -73,6 +74,7 @@ import org.schabi.newpipe.report.ErrorActivity; | |||||||
| import org.schabi.newpipe.report.UserAction; | import org.schabi.newpipe.report.UserAction; | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.InfoCache; | import org.schabi.newpipe.util.InfoCache; | ||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| @@ -581,30 +583,25 @@ public class VideoDetailFragment | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void initThumbnailViews(StreamInfo info) { |     private void initThumbnailViews(@NonNull StreamInfo info) { | ||||||
|         thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); |         thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); | ||||||
|         if (!TextUtils.isEmpty(info.getThumbnailUrl())) { |         if (!TextUtils.isEmpty(info.getThumbnailUrl())) { | ||||||
|             imageLoader.displayImage( |             final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); | ||||||
|                     info.getThumbnailUrl(), |             final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { | ||||||
|                     thumbnailImageView, |  | ||||||
|                     DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() { |  | ||||||
|                 @Override |                 @Override | ||||||
|                 public void onLoadingFailed(String imageUri, View view, FailReason failReason) { |                 public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||||
|                     ErrorActivity.reportError( |                     showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, | ||||||
|                             activity, |                             infoServiceName, imageUri, R.string.could_not_load_thumbnails); | ||||||
|                             failReason.getCause(), |  | ||||||
|                             null, |  | ||||||
|                             activity.findViewById(android.R.id.content), |  | ||||||
|                             ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, |  | ||||||
|                                     NewPipe.getNameOfService(currentInfo.getServiceId()), |  | ||||||
|                                     imageUri, |  | ||||||
|                                     R.string.could_not_load_thumbnails)); |  | ||||||
|                 } |                 } | ||||||
|             }); |             }; | ||||||
|  |  | ||||||
|  |             imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, | ||||||
|  |                     ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { |         if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { | ||||||
|             imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS); |             imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, | ||||||
|  |                     ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ import org.schabi.newpipe.report.UserAction; | |||||||
| import org.schabi.newpipe.subscription.SubscriptionService; | import org.schabi.newpipe.subscription.SubscriptionService; | ||||||
| import org.schabi.newpipe.util.AnimationUtils; | import org.schabi.newpipe.util.AnimationUtils; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  |  | ||||||
| @@ -419,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> { | |||||||
|         super.handleResult(result); |         super.handleResult(result); | ||||||
|  |  | ||||||
|         headerRootLayout.setVisibility(View.VISIBLE); |         headerRootLayout.setVisibility(View.VISIBLE); | ||||||
|         imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, DISPLAY_BANNER_OPTIONS); |         imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, | ||||||
|         imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, DISPLAY_AVATAR_OPTIONS); |         		ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); | ||||||
|  |         imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, | ||||||
|  |         		ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); | ||||||
|  |  | ||||||
|         if (result.getSubscriberCount() != -1) { |         if (result.getSubscriberCount() != -1) { | ||||||
|             headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); |             headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ import org.schabi.newpipe.playlist.PlaylistPlayQueue; | |||||||
| import org.schabi.newpipe.playlist.SinglePlayQueue; | import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.report.UserAction; | import org.schabi.newpipe.report.UserAction; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
| @@ -271,7 +272,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|  |  | ||||||
|         playlistCtrl.setVisibility(View.VISIBLE); |         playlistCtrl.setVisibility(View.VISIBLE); | ||||||
|  |  | ||||||
|         imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); |         imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, | ||||||
|  |                 ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); | ||||||
|         headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, |         headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, | ||||||
|                 (int) result.getStreamCount(), (int) result.getStreamCount())); |                 (int) result.getStreamCount(), (int) result.getStreamCount())); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| package org.schabi.newpipe.fragments.local; | package org.schabi.newpipe.fragments.local; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| import com.nostra13.universalimageloader.core.process.BitmapProcessor; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.util.OnClickGesture; | import org.schabi.newpipe.util.OnClickGesture; | ||||||
|   | |||||||
| @@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void showListFooter(final boolean show) { |     public void showListFooter(final boolean show) { | ||||||
|         itemsList.post(() -> itemListAdapter.showFooter(show)); |         if (itemsList == null) return; | ||||||
|  |         itemsList.post(() -> { | ||||||
|  |             if (itemListAdapter != null) itemListAdapter.showFooter(show); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -1,14 +1,8 @@ | |||||||
| package org.schabi.newpipe.fragments.local.holder; | package org.schabi.newpipe.fragments.local.holder; | ||||||
|  |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.support.annotation.DimenRes; |  | ||||||
| import android.support.v7.widget.RecyclerView; | import android.support.v7.widget.RecyclerView; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.ImageView; |  | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
| import com.nostra13.universalimageloader.core.process.BitmapProcessor; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
| @@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); |     public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // ImageLoaderOptions |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Base display options |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cacheInMemory(true) |  | ||||||
|                     .cacheOnDisk(true) |  | ||||||
|                     .bitmapConfig(Bitmap.Config.RGB_565) |  | ||||||
|                     .resetViewBeforeLoading(false) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,15 +2,11 @@ package org.schabi.newpipe.fragments.local.holder; | |||||||
|  |  | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; |  | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
|  |  | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
|  |  | ||||||
| @@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { | |||||||
|         itemStreamCountView.setText(String.valueOf(item.streamCount)); |         itemStreamCountView.setText(String.valueOf(item.streamCount)); | ||||||
|         itemUploaderView.setVisibility(View.INVISIBLE); |         itemUploaderView.setVisibility(View.INVISIBLE); | ||||||
|  |  | ||||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); |         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, | ||||||
|  |                 ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); | ||||||
|  |  | ||||||
|         super.updateFromItem(localItem, dateFormat); |         super.updateFromItem(localItem, dateFormat); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| package org.schabi.newpipe.fragments.local.holder; | package org.schabi.newpipe.fragments.local.holder; | ||||||
|  |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.support.v4.content.ContextCompat; | import android.support.v4.content.ContextCompat; | ||||||
| import android.view.MotionEvent; | import android.view.MotionEvent; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| @@ -8,14 +7,12 @@ import android.view.ViewGroup; | |||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
| import com.nostra13.universalimageloader.core.assist.ImageScaleType; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
| @@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Default thumbnail is shown on error, while loading and if the url is empty |         // Default thumbnail is shown on error, while loading and if the url is empty | ||||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); |         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, | ||||||
|  |                 ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(view -> { |         itemView.setOnClickListener(view -> { | ||||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { |             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||||
| @@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { | |||||||
|             return false; |             return false; | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for stream thumbnails |  | ||||||
|      */ |  | ||||||
|     private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,13 +6,12 @@ import android.view.ViewGroup; | |||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
| @@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { | |||||||
|         itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); |         itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); | ||||||
|  |  | ||||||
|         // Default thumbnail is shown on error, while loading and if the url is empty |         // Default thumbnail is shown on error, while loading and if the url is empty | ||||||
|         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); |         itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, | ||||||
|  |                 ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(view -> { |         itemView.setOnClickListener(view -> { | ||||||
|             if (itemBuilder.getOnItemSelectedListener() != null) { |             if (itemBuilder.getOnItemSelectedListener() != null) { | ||||||
| @@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { | |||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for stream thumbnails |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ import android.view.ViewGroup; | |||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.LocalItem; | import org.schabi.newpipe.database.LocalItem; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
| @@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { | |||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for playlist thumbnails |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem; | |||||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.fragments.local.LocalItemBuilder; | import org.schabi.newpipe.fragments.local.LocalItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
| @@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { | |||||||
|                 NewPipe.getNameOfService(item.getServiceId()))); |                 NewPipe.getNameOfService(item.getServiceId()))); | ||||||
|  |  | ||||||
|         itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, |         itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, | ||||||
|                 DISPLAY_THUMBNAIL_OPTIONS); |                 ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); | ||||||
|  |  | ||||||
|         super.updateFromItem(localItem, dateFormat); |         super.updateFromItem(localItem, dateFormat); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; | |||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.history.model.StreamHistoryEntry; | import org.schabi.newpipe.database.history.model.StreamHistoryEntry; | ||||||
| import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; | import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  |  | ||||||
| @@ -147,7 +148,7 @@ public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> { | |||||||
|             holder.uploader.setText(entry.uploader); |             holder.uploader.setText(entry.uploader); | ||||||
|             holder.duration.setText(Localization.getDurationString(entry.duration)); |             holder.duration.setText(Localization.getDurationString(entry.duration)); | ||||||
|             ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, |             ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, | ||||||
|                     StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); |                     ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,13 @@ | |||||||
| package org.schabi.newpipe.info_list.holder; | package org.schabi.newpipe.info_list.holder; | ||||||
|  |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
| import de.hdodenhof.circleimageview.CircleImageView; | import de.hdodenhof.circleimageview.CircleImageView; | ||||||
| @@ -42,7 +40,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         itemBuilder.getImageLoader() |         itemBuilder.getImageLoader() | ||||||
|                 .displayImage(item.getThumbnailUrl(), |                 .displayImage(item.getThumbnailUrl(), | ||||||
|                         itemThumbnailView, |                         itemThumbnailView, | ||||||
|                         ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); |                         ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(view -> { |         itemView.setOnClickListener(view -> { | ||||||
|             if (itemBuilder.getOnChannelSelectedListener() != null) { |             if (itemBuilder.getOnChannelSelectedListener() != null) { | ||||||
| @@ -59,15 +57,4 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         } |         } | ||||||
|         return details; |         return details; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for channel thumbnails |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnLoading(R.drawable.buddy_channel_item) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.buddy_channel_item) |  | ||||||
|                     .showImageOnFail(R.drawable.buddy_channel_item) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView; | |||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  |  | ||||||
| @@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public abstract void updateFromItem(final InfoItem infoItem); |     public abstract void updateFromItem(final InfoItem infoItem); | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // ImageLoaderOptions |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Base display options |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cacheInMemory(true) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,12 +4,11 @@ import android.view.ViewGroup; | |||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
|  |  | ||||||
| public class PlaylistMiniInfoItemHolder extends InfoItemHolder { | public class PlaylistMiniInfoItemHolder extends InfoItemHolder { | ||||||
|     public final ImageView itemThumbnailView; |     public final ImageView itemThumbnailView; | ||||||
| @@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         itemUploaderView.setText(item.getUploaderName()); |         itemUploaderView.setText(item.getUploaderName()); | ||||||
|  |  | ||||||
|         itemBuilder.getImageLoader() |         itemBuilder.getImageLoader() | ||||||
|                 .displayImage(item.getThumbnailUrl(), itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); |                 .displayImage(item.getThumbnailUrl(), itemThumbnailView, | ||||||
|  |                 		ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(view -> { |         itemView.setOnClickListener(view -> { | ||||||
|             if (itemBuilder.getOnPlaylistSelectedListener() != null) { |             if (itemBuilder.getOnPlaylistSelectedListener() != null) { | ||||||
| @@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { | |||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for playlist thumbnails |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,13 +6,12 @@ import android.view.ViewGroup; | |||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
| public class StreamMiniInfoItemHolder extends InfoItemHolder { | public class StreamMiniInfoItemHolder extends InfoItemHolder { | ||||||
| @@ -61,7 +60,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         itemBuilder.getImageLoader() |         itemBuilder.getImageLoader() | ||||||
|                 .displayImage(item.getThumbnailUrl(), |                 .displayImage(item.getThumbnailUrl(), | ||||||
|                         itemThumbnailView, |                         itemThumbnailView, | ||||||
|                         StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); |                         ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(view -> { |         itemView.setOnClickListener(view -> { | ||||||
|             if (itemBuilder.getOnStreamSelectedListener() != null) { |             if (itemBuilder.getOnStreamSelectedListener() != null) { | ||||||
| @@ -98,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         itemView.setLongClickable(false); |         itemView.setLongClickable(false); | ||||||
|         itemView.setOnLongClickListener(null); |         itemView.setOnLongClickListener(null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display options for stream thumbnails |  | ||||||
|      */ |  | ||||||
|     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = |  | ||||||
|             new DisplayImageOptions.Builder() |  | ||||||
|                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) |  | ||||||
|                     .showImageOnFail(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) |  | ||||||
|                     .showImageOnLoading(R.drawable.dummy_thumbnail) |  | ||||||
|                     .build(); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,8 +22,6 @@ package org.schabi.newpipe.player; | |||||||
| import android.app.NotificationManager; | import android.app.NotificationManager; | ||||||
| import android.app.PendingIntent; | import android.app.PendingIntent; | ||||||
| import android.app.Service; | import android.app.Service; | ||||||
| import android.content.BroadcastReceiver; |  | ||||||
| import android.content.ComponentName; |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.IntentFilter; | import android.content.IntentFilter; | ||||||
| @@ -35,7 +33,6 @@ import android.support.annotation.NonNull; | |||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.NotificationCompat; | import android.support.v4.app.NotificationCompat; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.KeyEvent; |  | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.RemoteViews; | import android.widget.RemoteViews; | ||||||
|  |  | ||||||
| @@ -81,8 +78,6 @@ public final class BackgroundPlayer extends Service { | |||||||
|     private BasePlayerImpl basePlayerImpl; |     private BasePlayerImpl basePlayerImpl; | ||||||
|     private LockManager lockManager; |     private LockManager lockManager; | ||||||
|  |  | ||||||
|     private ComponentName mReceiverComponent; |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Service-Activity Binder |     // Service-Activity Binder | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -119,9 +114,6 @@ public final class BackgroundPlayer extends Service { | |||||||
|  |  | ||||||
|         mBinder = new PlayerServiceBinder(basePlayerImpl); |         mBinder = new PlayerServiceBinder(basePlayerImpl); | ||||||
|         shouldUpdateOnProgress = true; |         shouldUpdateOnProgress = true; | ||||||
|  |  | ||||||
|         mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class); |  | ||||||
|         basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -152,7 +144,6 @@ public final class BackgroundPlayer extends Service { | |||||||
|             lockManager.releaseWifiAndCpu(); |             lockManager.releaseWifiAndCpu(); | ||||||
|         } |         } | ||||||
|         if (basePlayerImpl != null) { |         if (basePlayerImpl != null) { | ||||||
|             basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent); |  | ||||||
|             basePlayerImpl.stopActivityBinding(); |             basePlayerImpl.stopActivityBinding(); | ||||||
|             basePlayerImpl.destroy(); |             basePlayerImpl.destroy(); | ||||||
|         } |         } | ||||||
| @@ -495,7 +486,7 @@ public final class BackgroundPlayer extends Service { | |||||||
|                     onClose(); |                     onClose(); | ||||||
|                     break; |                     break; | ||||||
|                 case ACTION_PLAY_PAUSE: |                 case ACTION_PLAY_PAUSE: | ||||||
|                     onVideoPlayPause(); |                     onPlayPause(); | ||||||
|                     break; |                     break; | ||||||
|                 case ACTION_REPEAT: |                 case ACTION_REPEAT: | ||||||
|                     onRepeatClicked(); |                     onRepeatClicked(); | ||||||
| @@ -573,41 +564,4 @@ public final class BackgroundPlayer extends Service { | |||||||
|             lockManager.releaseWifiAndCpu(); |             lockManager.releaseWifiAndCpu(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static class MediaButtonReceiver extends BroadcastReceiver { |  | ||||||
|  |  | ||||||
|         public MediaButtonReceiver() { |  | ||||||
|             super(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public void onReceive(Context context, Intent intent) { |  | ||||||
|             if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { |  | ||||||
|                 KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); |  | ||||||
|                 if (event.getAction() == KeyEvent.ACTION_UP) { |  | ||||||
|                     int keycode = event.getKeyCode(); |  | ||||||
|                     PendingIntent pendingIntent = null; |  | ||||||
|                     if (keycode == KeyEvent.KEYCODE_MEDIA_NEXT) { |  | ||||||
|                         pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|                     } else if (keycode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) { |  | ||||||
|                         pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|                     } else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PAUSE || keycode == KeyEvent.KEYCODE_MEDIA_PLAY) { |  | ||||||
|                         pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|                     } else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { |  | ||||||
|                         pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|                     } else if (keycode == KeyEvent.KEYCODE_MEDIA_REWIND) { |  | ||||||
|                         pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|                     } |  | ||||||
|                     if (pendingIntent != null) { |  | ||||||
|                         try { |  | ||||||
|                             pendingIntent.send(); |  | ||||||
|                         } catch (Exception e) { |  | ||||||
|                             Log.e(TAG, "Error Sending intent MediaButtonReceiver", e); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -57,11 +57,14 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | |||||||
| import org.schabi.newpipe.Downloader; | import org.schabi.newpipe.Downloader; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.history.HistoryRecordManager; | import org.schabi.newpipe.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.player.helper.AudioReactor; | import org.schabi.newpipe.player.helper.AudioReactor; | ||||||
| import org.schabi.newpipe.player.helper.LoadController; | import org.schabi.newpipe.player.helper.LoadController; | ||||||
|  | import org.schabi.newpipe.player.helper.MediaSessionManager; | ||||||
| import org.schabi.newpipe.player.helper.PlayerDataSource; | import org.schabi.newpipe.player.helper.PlayerDataSource; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
|  | import org.schabi.newpipe.player.playback.BasePlayerMediaSession; | ||||||
| import org.schabi.newpipe.player.playback.CustomTrackSelector; | import org.schabi.newpipe.player.playback.CustomTrackSelector; | ||||||
| import org.schabi.newpipe.player.playback.MediaSourceManager; | import org.schabi.newpipe.player.playback.MediaSourceManager; | ||||||
| import org.schabi.newpipe.player.playback.PlaybackListener; | import org.schabi.newpipe.player.playback.PlaybackListener; | ||||||
| @@ -147,8 +150,10 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|     protected SimpleExoPlayer simpleExoPlayer; |     protected SimpleExoPlayer simpleExoPlayer; | ||||||
|     protected AudioReactor audioReactor; |     protected AudioReactor audioReactor; | ||||||
|  |     protected MediaSessionManager mediaSessionManager; | ||||||
|  |  | ||||||
|     protected boolean isPrepared = false; |     private boolean isPrepared = false; | ||||||
|  |     private boolean isSynchronizing = false; | ||||||
|  |  | ||||||
|     protected Disposable progressUpdateReactor; |     protected Disposable progressUpdateReactor; | ||||||
|     protected CompositeDisposable databaseUpdateReactor; |     protected CompositeDisposable databaseUpdateReactor; | ||||||
| @@ -193,11 +198,13 @@ public abstract class BasePlayer implements | |||||||
|         final LoadControl loadControl = new LoadController(context); |         final LoadControl loadControl = new LoadController(context); | ||||||
|         final RenderersFactory renderFactory = new DefaultRenderersFactory(context); |         final RenderersFactory renderFactory = new DefaultRenderersFactory(context); | ||||||
|         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); |         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); | ||||||
|         audioReactor = new AudioReactor(context, simpleExoPlayer); |  | ||||||
|  |  | ||||||
|         simpleExoPlayer.addListener(this); |         simpleExoPlayer.addListener(this); | ||||||
|         simpleExoPlayer.setPlayWhenReady(true); |         simpleExoPlayer.setPlayWhenReady(true); | ||||||
|         simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); |         simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); | ||||||
|  |  | ||||||
|  |         audioReactor = new AudioReactor(context, simpleExoPlayer); | ||||||
|  |         mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, | ||||||
|  |                 new BasePlayerMediaSession(this)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void initListeners() {} |     public void initListeners() {} | ||||||
| @@ -244,6 +251,7 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|         playQueue = queue; |         playQueue = queue; | ||||||
|         playQueue.init(); |         playQueue.init(); | ||||||
|  |         if (playbackManager != null) playbackManager.dispose(); | ||||||
|         playbackManager = new MediaSourceManager(this, playQueue); |         playbackManager = new MediaSourceManager(this, playQueue); | ||||||
|  |  | ||||||
|         if (playQueueAdapter != null) playQueueAdapter.dispose(); |         if (playQueueAdapter != null) playQueueAdapter.dispose(); | ||||||
| @@ -259,8 +267,8 @@ public abstract class BasePlayer implements | |||||||
|         } |         } | ||||||
|         if (isProgressLoopRunning()) stopProgressLoop(); |         if (isProgressLoopRunning()) stopProgressLoop(); | ||||||
|         if (playQueue != null) playQueue.dispose(); |         if (playQueue != null) playQueue.dispose(); | ||||||
|  |         if (audioReactor != null) audioReactor.dispose(); | ||||||
|         if (playbackManager != null) playbackManager.dispose(); |         if (playbackManager != null) playbackManager.dispose(); | ||||||
|         if (audioReactor != null) audioReactor.abandonAudioFocus(); |  | ||||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); |         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||||
|  |  | ||||||
|         if (playQueueAdapter != null) { |         if (playQueueAdapter != null) { | ||||||
| @@ -272,11 +280,11 @@ public abstract class BasePlayer implements | |||||||
|     public void destroy() { |     public void destroy() { | ||||||
|         if (DEBUG) Log.d(TAG, "destroy() called"); |         if (DEBUG) Log.d(TAG, "destroy() called"); | ||||||
|         destroyPlayer(); |         destroyPlayer(); | ||||||
|         clearThumbnailCache(); |  | ||||||
|         unregisterBroadcastReceiver(); |         unregisterBroadcastReceiver(); | ||||||
|  |  | ||||||
|         trackSelector = null; |         trackSelector = null; | ||||||
|         simpleExoPlayer = null; |         simpleExoPlayer = null; | ||||||
|  |         mediaSessionManager = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -314,11 +322,6 @@ public abstract class BasePlayer implements | |||||||
|         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + |         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + | ||||||
|                 "imageUri = [" + imageUri + "], view = [" + view + "]"); |                 "imageUri = [" + imageUri + "], view = [" + view + "]"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void clearThumbnailCache() { |  | ||||||
|         ImageLoader.getInstance().clearMemoryCache(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // MediaSource Building |     // MediaSource Building | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -389,7 +392,7 @@ public abstract class BasePlayer implements | |||||||
|         if (intent == null || intent.getAction() == null) return; |         if (intent == null || intent.getAction() == null) return; | ||||||
|         switch (intent.getAction()) { |         switch (intent.getAction()) { | ||||||
|             case AudioManager.ACTION_AUDIO_BECOMING_NOISY: |             case AudioManager.ACTION_AUDIO_BECOMING_NOISY: | ||||||
|                 if (isPlaying()) onVideoPlayPause(); |                 onPause(); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -406,6 +409,7 @@ public abstract class BasePlayer implements | |||||||
|     // States Implementation |     // States Implementation | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     public static final int STATE_PREFLIGHT = -1; | ||||||
|     public static final int STATE_BLOCKED = 123; |     public static final int STATE_BLOCKED = 123; | ||||||
|     public static final int STATE_PLAYING = 124; |     public static final int STATE_PLAYING = 124; | ||||||
|     public static final int STATE_BUFFERING = 125; |     public static final int STATE_BUFFERING = 125; | ||||||
| @@ -413,7 +417,7 @@ public abstract class BasePlayer implements | |||||||
|     public static final int STATE_PAUSED_SEEK = 127; |     public static final int STATE_PAUSED_SEEK = 127; | ||||||
|     public static final int STATE_COMPLETED = 128; |     public static final int STATE_COMPLETED = 128; | ||||||
|  |  | ||||||
|     protected int currentState = -1; |     protected int currentState = STATE_PREFLIGHT; | ||||||
|  |  | ||||||
|     public void changeState(int state) { |     public void changeState(int state) { | ||||||
|         if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); |         if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); | ||||||
| @@ -448,7 +452,6 @@ public abstract class BasePlayer implements | |||||||
|     public void onPlaying() { |     public void onPlaying() { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlaying() called"); |         if (DEBUG) Log.d(TAG, "onPlaying() called"); | ||||||
|         if (!isProgressLoopRunning()) startProgressLoop(); |         if (!isProgressLoopRunning()) startProgressLoop(); | ||||||
|         if (!isCurrentWindowValid()) seekToDefault(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onBuffering() {} |     public void onBuffering() {} | ||||||
| @@ -522,11 +525,9 @@ public abstract class BasePlayer implements | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private Disposable getProgressReactor() { |     private Disposable getProgressReactor() { | ||||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) |         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .filter(ignored -> isProgressLoopRunning()) |  | ||||||
|                 .subscribe(ignored -> triggerProgressUpdate()); |                 .subscribe(ignored -> triggerProgressUpdate()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -541,16 +542,21 @@ public abstract class BasePlayer implements | |||||||
|                 (manifest == null ? "no manifest" : "available manifest") + ", " + |                 (manifest == null ? "no manifest" : "available manifest") + ", " + | ||||||
|                 "timeline size = [" + timeline.getWindowCount() + "], " + |                 "timeline size = [" + timeline.getWindowCount() + "], " + | ||||||
|                 "reason = [" + reason + "]"); |                 "reason = [" + reason + "]"); | ||||||
|  |         if (playQueue == null) return; | ||||||
|  |  | ||||||
|         switch (reason) { |         switch (reason) { | ||||||
|             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block |             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block | ||||||
|             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock |             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock | ||||||
|             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes |             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes | ||||||
|                 if (playQueue != null && playbackManager != null && |                 // Ensures MediaSourceManager#update is complete | ||||||
|                         // ensures MediaSourceManager#update is complete |                 final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size(); | ||||||
|                         timeline.getWindowCount() == playQueue.size()) { |                 // Ensure dynamic/livestream timeline changes does not cause negative position | ||||||
|                     playbackManager.load(); |                 if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) { | ||||||
|  |                     if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " + | ||||||
|  |                             "clamping position to 0ms."); | ||||||
|  |                     seekTo(/*clampToTime=*/0); | ||||||
|                 } |                 } | ||||||
|  |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -600,49 +606,54 @@ public abstract class BasePlayer implements | |||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
|             case Player.STATE_READY: //3 |             case Player.STATE_READY: //3 | ||||||
|                 maybeRecover(); |                 maybeCorrectSeekPosition(); | ||||||
|                 if (!isPrepared) { |                 if (!isPrepared) { | ||||||
|                     isPrepared = true; |                     isPrepared = true; | ||||||
|                     onPrepared(playWhenReady); |                     onPrepared(playWhenReady); | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|                 if (currentState == STATE_PAUSED_SEEK) break; |  | ||||||
|                 changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |                 changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|                 break; |                 break; | ||||||
|             case Player.STATE_ENDED: // 4 |             case Player.STATE_ENDED: // 4 | ||||||
|                 // Ensure the current window has actually ended |  | ||||||
|                 // since single windows that are still loading may produce an ended state |  | ||||||
|                 if (isCurrentWindowValid() && |  | ||||||
|                         simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { |  | ||||||
|                 changeState(STATE_COMPLETED); |                 changeState(STATE_COMPLETED); | ||||||
|                 isPrepared = false; |                 isPrepared = false; | ||||||
|                 } |  | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void maybeRecover() { |     private void maybeCorrectSeekPosition() { | ||||||
|  |         if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return; | ||||||
|  |  | ||||||
|         final int currentSourceIndex = playQueue.getIndex(); |         final int currentSourceIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentSourceItem = playQueue.getItem(); |         final PlayQueueItem currentSourceItem = playQueue.getItem(); | ||||||
|  |         if (currentSourceItem == null) return; | ||||||
|  |  | ||||||
|         // Check if already playing correct window |         final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition(); | ||||||
|         final boolean isCurrentPeriodCorrect = |         final boolean isCurrentWindowCorrect = | ||||||
|                 simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; |                 simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; | ||||||
|  |         final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; | ||||||
|  |  | ||||||
|         // Check if recovering |         if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) { | ||||||
|         if (isCurrentPeriodCorrect && currentSourceItem != null) { |             // Is recovering previous playback? | ||||||
|             /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, |             if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" + | ||||||
|              * rounding this position to the nearest second will help alleviate this.*/ |                     "[" + getTimeString((int)recoveryPositionMillis) + "]"); | ||||||
|             final long position = currentSourceItem.getRecoveryPosition(); |             seekTo(recoveryPositionMillis); | ||||||
|  |  | ||||||
|             /* Skip recovering if the recovery position is not set.*/ |  | ||||||
|             if (position == PlayQueueItem.RECOVERY_UNSET) return; |  | ||||||
|  |  | ||||||
|             if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + |  | ||||||
|                     " at: " + getTimeString((int)position)); |  | ||||||
|             simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); |  | ||||||
|             playQueue.unsetRecovery(currentSourceIndex); |             playQueue.unsetRecovery(currentSourceIndex); | ||||||
|  |  | ||||||
|  |         } else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time"); | ||||||
|  |             // Is still synchronizing? | ||||||
|  |             seekToDefault(); | ||||||
|  |  | ||||||
|  |         } else if (isSynchronizing && presetStartPositionMillis != 0L) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " + | ||||||
|  |                     "position=[" + presetStartPositionMillis + "]"); | ||||||
|  |             // Has another start position? | ||||||
|  |             seekTo(presetStartPositionMillis); | ||||||
|  |             currentInfo.setStartPosition(0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         isSynchronizing = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -775,6 +786,16 @@ public abstract class BasePlayer implements | |||||||
|     // Playback Listener |     // Playback Listener | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isNearPlaybackEdge(final long timeToEndMillis) { | ||||||
|  |         // If live, then not near playback edge | ||||||
|  |         if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false; | ||||||
|  |  | ||||||
|  |         final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); | ||||||
|  |         final long currentDurationMillis = simpleExoPlayer.getDuration(); | ||||||
|  |         return currentDurationMillis - currentPositionMillis < timeToEndMillis; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onPlaybackBlock() { |     public void onPlaybackBlock() { | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
| @@ -796,7 +817,6 @@ public abstract class BasePlayer implements | |||||||
|         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); |         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); | ||||||
|  |  | ||||||
|         simpleExoPlayer.prepare(mediaSource); |         simpleExoPlayer.prepare(mediaSource); | ||||||
|         seekToDefault(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -805,11 +825,26 @@ public abstract class BasePlayer implements | |||||||
|         if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + |         if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + | ||||||
|                 (info != null ? "available" : "null") + " info, " + |                 (info != null ? "available" : "null") + " info, " + | ||||||
|                 "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); |                 "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); | ||||||
|  |         if (simpleExoPlayer == null || playQueue == null) return; | ||||||
|  |  | ||||||
|  |         final boolean onPlaybackInitial = currentItem == null; | ||||||
|         final boolean hasPlayQueueItemChanged = currentItem != item; |         final boolean hasPlayQueueItemChanged = currentItem != item; | ||||||
|         final boolean hasStreamInfoChanged = currentInfo != info; |         final boolean hasStreamInfoChanged = currentInfo != info; | ||||||
|  |  | ||||||
|  |         final int currentPlayQueueIndex = playQueue.indexOf(item); | ||||||
|  |         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||||
|  |         final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); | ||||||
|  |  | ||||||
|  |         // when starting playback on the last item when not repeating, maybe auto queue | ||||||
|  |         if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && | ||||||
|  |                 getRepeatMode() == Player.REPEAT_MODE_OFF && | ||||||
|  |                 PlayerHelper.isAutoQueueEnabled(context)) { | ||||||
|  |             final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); | ||||||
|  |             if (autoQueue != null) playQueue.append(autoQueue.getStreams()); | ||||||
|  |         } | ||||||
|  |         // If nothing to synchronize | ||||||
|         if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { |         if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { | ||||||
|             return; // Nothing to synchronize |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         currentItem = item; |         currentItem = item; | ||||||
| @@ -819,34 +854,31 @@ public abstract class BasePlayer implements | |||||||
|             registerView(); |             registerView(); | ||||||
|             initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); |             initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         final int currentPlayQueueIndex = playQueue.indexOf(item); |  | ||||||
|         onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); |         onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); | ||||||
|  |  | ||||||
|         if (simpleExoPlayer == null) return; |  | ||||||
|         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); |  | ||||||
|         // Check if on wrong window |         // Check if on wrong window | ||||||
|         if (currentPlayQueueIndex != playQueue.getIndex()) { |         if (currentPlayQueueIndex != playQueue.getIndex()) { | ||||||
|             Log.e(TAG, "Play Queue may be desynchronized: item " + |             Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + | ||||||
|                     "index=[" + currentPlayQueueIndex + "], " + |                     "index=[" + currentPlayQueueIndex + "], " + | ||||||
|                     "queue index=[" + playQueue.getIndex() + "]"); |                     "queue index=[" + playQueue.getIndex() + "]"); | ||||||
|  |  | ||||||
|             // on metadata changed |             // Check if bad seek position | ||||||
|         } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { |         } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) || | ||||||
|             final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; |                 currentPlayQueueIndex < 0) { | ||||||
|             if (DEBUG) Log.d(TAG, "Rewinding to correct" + |             Log.e(TAG, "Playback - Trying to seek to invalid " + | ||||||
|                     " window=[" + currentPlayQueueIndex + "]," + |                     "index=[" + currentPlayQueueIndex + "] with " + | ||||||
|                     " at=[" + getTimeString((int)startPos) + "]," + |                     "playlist length=[" + currentPlaylistSize + "]"); | ||||||
|                     " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); |  | ||||||
|             simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // when starting playback on the last item when not repeating, maybe auto queue |             // If not playing correct stream, change window position and sets flag | ||||||
|         if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && |             // for synchronizing once window position is corrected | ||||||
|                 getRepeatMode() == Player.REPEAT_MODE_OFF && |             // @see maybeCorrectSeekPosition() | ||||||
|                 PlayerHelper.isAutoQueueEnabled(context)) { |         } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial || | ||||||
|             final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); |                 !isPlaying()) { | ||||||
|             if (autoQueue != null) playQueue.append(autoQueue.getStreams()); |             if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" + | ||||||
|  |                     " index=[" + currentPlayQueueIndex + "]," + | ||||||
|  |                     " from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "]."); | ||||||
|  |             isSynchronizing = true; | ||||||
|  |             simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -858,6 +890,11 @@ public abstract class BasePlayer implements | |||||||
|     @Nullable |     @Nullable | ||||||
|     @Override |     @Override | ||||||
|     public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { |     public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { | ||||||
|  |         final StreamType streamType = info.getStreamType(); | ||||||
|  |         if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (!info.getHlsUrl().isEmpty()) { |         if (!info.getHlsUrl().isEmpty()) { | ||||||
|             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); |             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); | ||||||
|         } else if (!info.getDashMpdUrl().isEmpty()) { |         } else if (!info.getDashMpdUrl().isEmpty()) { | ||||||
| @@ -911,14 +948,11 @@ public abstract class BasePlayer implements | |||||||
|         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onVideoPlayPause() { |     public void onPlay() { | ||||||
|         if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); |         if (DEBUG) Log.d(TAG, "onPlay() called"); | ||||||
|  |         if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return; | ||||||
|  |  | ||||||
|         if (!isPlaying()) { |  | ||||||
|         audioReactor.requestAudioFocus(); |         audioReactor.requestAudioFocus(); | ||||||
|         } else { |  | ||||||
|             audioReactor.abandonAudioFocus(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (getCurrentState() == STATE_COMPLETED) { |         if (getCurrentState() == STATE_COMPLETED) { | ||||||
|             if (playQueue.getIndex() == 0) { |             if (playQueue.getIndex() == 0) { | ||||||
| @@ -928,7 +962,25 @@ public abstract class BasePlayer implements | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         simpleExoPlayer.setPlayWhenReady(!isPlaying()); |         simpleExoPlayer.setPlayWhenReady(true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void onPause() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onPause() called"); | ||||||
|  |         if (audioReactor == null || simpleExoPlayer == null) return; | ||||||
|  |  | ||||||
|  |         audioReactor.abandonAudioFocus(); | ||||||
|  |         simpleExoPlayer.setPlayWhenReady(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void onPlayPause() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onPlayPause() called"); | ||||||
|  |  | ||||||
|  |         if (!isPlaying()) { | ||||||
|  |             onPlay(); | ||||||
|  |         } else { | ||||||
|  |             onPause(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onFastRewind() { |     public void onFastRewind() { | ||||||
| @@ -945,14 +997,15 @@ public abstract class BasePlayer implements | |||||||
|         if (simpleExoPlayer == null || playQueue == null) return; |         if (simpleExoPlayer == null || playQueue == null) return; | ||||||
|         if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); |         if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); | ||||||
|  |  | ||||||
|         savePlaybackState(); |         /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, | ||||||
|  |         * restart current track. Also restart the track if the current track | ||||||
|         /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. |         * is the first in a queue.*/ | ||||||
|         * Also restart the track if the current track is the first in a queue.*/ |         if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || | ||||||
|         if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { |                 playQueue.getIndex() == 0) { | ||||||
|             final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); |             seekToDefault(); | ||||||
|             simpleExoPlayer.seekTo(startPos); |             playQueue.offsetIndex(0); | ||||||
|         } else { |         } else { | ||||||
|  |             savePlaybackState(); | ||||||
|             playQueue.offsetIndex(-1); |             playQueue.offsetIndex(-1); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -962,7 +1015,6 @@ public abstract class BasePlayer implements | |||||||
|         if (DEBUG) Log.d(TAG, "onPlayNext() called"); |         if (DEBUG) Log.d(TAG, "onPlayNext() called"); | ||||||
|  |  | ||||||
|         savePlaybackState(); |         savePlaybackState(); | ||||||
|  |  | ||||||
|         playQueue.offsetIndex(+1); |         playQueue.offsetIndex(+1); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -975,20 +1027,21 @@ public abstract class BasePlayer implements | |||||||
|         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { |         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { | ||||||
|             seekToDefault(); |             seekToDefault(); | ||||||
|         } else { |         } else { | ||||||
|  |             savePlaybackState(); | ||||||
|  |         } | ||||||
|         playQueue.setIndex(index); |         playQueue.setIndex(index); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void seekTo(long positionMillis) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); | ||||||
|  |         if (simpleExoPlayer == null || positionMillis < 0 || | ||||||
|  |                 positionMillis > simpleExoPlayer.getDuration()) return; | ||||||
|  |         simpleExoPlayer.seekTo(positionMillis); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void seekBy(int milliSeconds) { |     public void seekBy(long offsetMillis) { | ||||||
|         if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); |         if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); | ||||||
|         if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || |         seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); | ||||||
|                 ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); |  | ||||||
|         if (progress < 0) progress = 0; |  | ||||||
|         simpleExoPlayer.seekTo(progress); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isCurrentWindowValid() { |     public boolean isCurrentWindowValid() { | ||||||
| @@ -1015,8 +1068,11 @@ public abstract class BasePlayer implements | |||||||
|  |  | ||||||
|     protected void reload() { |     protected void reload() { | ||||||
|         if (playbackManager != null) { |         if (playbackManager != null) { | ||||||
|             playbackManager.reset(); |             playbackManager.dispose(); | ||||||
|             playbackManager.load(); |         } | ||||||
|  |  | ||||||
|  |         if (playQueue != null) { | ||||||
|  |             playbackManager = new MediaSourceManager(this, playQueue); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1069,8 +1125,22 @@ public abstract class BasePlayer implements | |||||||
|         return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); |         return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isCompleted() { |     /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */ | ||||||
|         return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED; |     public boolean isLiveEdge() { | ||||||
|  |         if (simpleExoPlayer == null) return false; | ||||||
|  |         final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic(); | ||||||
|  |         if (!isLive) return false; | ||||||
|  |  | ||||||
|  |         final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); | ||||||
|  |         final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||||
|  |         if (currentTimeline.isEmpty() || currentWindowIndex < 0 || | ||||||
|  |                 currentWindowIndex >= currentTimeline.getWindowCount()) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Timeline.Window timelineWindow = new Timeline.Window(); | ||||||
|  |         currentTimeline.getWindow(currentWindowIndex, timelineWindow); | ||||||
|  |         return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isPlaying() { |     public boolean isPlaying() { | ||||||
| @@ -1123,8 +1193,8 @@ public abstract class BasePlayer implements | |||||||
|         return playQueueAdapter; |         return playQueueAdapter; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isPlayerReady() { |     public boolean isPrepared() { | ||||||
|         return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; |         return isPrepared; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isProgressLoopRunning() { |     public boolean isProgressLoopRunning() { | ||||||
|   | |||||||
| @@ -19,7 +19,6 @@ | |||||||
|  |  | ||||||
| package org.schabi.newpipe.player; | package org.schabi.newpipe.player; | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| @@ -33,6 +32,7 @@ import android.preference.PreferenceManager; | |||||||
| import android.provider.Settings; | import android.provider.Settings; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v7.app.AppCompatActivity; | ||||||
| import android.support.v7.widget.RecyclerView; | import android.support.v7.widget.RecyclerView; | ||||||
| import android.support.v7.widget.helper.ItemTouchHelper; | import android.support.v7.widget.helper.ItemTouchHelper; | ||||||
| import android.util.DisplayMetrics; | import android.util.DisplayMetrics; | ||||||
| @@ -57,11 +57,13 @@ import org.schabi.newpipe.R; | |||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||||
|  | import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
| import org.schabi.newpipe.playlist.PlayQueue; | import org.schabi.newpipe.playlist.PlayQueue; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemHolder; | import org.schabi.newpipe.playlist.PlayQueueItemHolder; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; | ||||||
| import org.schabi.newpipe.util.AnimationUtils; | import org.schabi.newpipe.util.AnimationUtils; | ||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| @@ -76,6 +78,8 @@ import java.util.UUID; | |||||||
| import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; | import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; | ||||||
| import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | ||||||
| import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; | ||||||
|  | import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; | ||||||
|  | import static org.schabi.newpipe.util.AnimationUtils.animateRotation; | ||||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
| import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; | import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; | ||||||
|  |  | ||||||
| @@ -84,7 +88,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; | |||||||
|  * |  * | ||||||
|  * @author mauriciocolli |  * @author mauriciocolli | ||||||
|  */ |  */ | ||||||
| public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { | public final class MainVideoPlayer extends AppCompatActivity | ||||||
|  |         implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { | ||||||
|     private static final String TAG = ".MainVideoPlayer"; |     private static final String TAG = ".MainVideoPlayer"; | ||||||
|     private static final boolean DEBUG = BasePlayer.DEBUG; |     private static final boolean DEBUG = BasePlayer.DEBUG; | ||||||
|  |  | ||||||
| @@ -110,7 +115,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); | ||||||
|         setVolumeControlStream(AudioManager.STREAM_MUSIC); |         setVolumeControlStream(AudioManager.STREAM_MUSIC); | ||||||
|  |  | ||||||
|         changeSystemUi(); |         hideSystemUi(); | ||||||
|         setContentView(R.layout.activity_main_player); |         setContentView(R.layout.activity_main_player); | ||||||
|         playerImpl = new VideoPlayerImpl(this); |         playerImpl = new VideoPlayerImpl(this); | ||||||
|         playerImpl.setup(findViewById(android.R.id.content)); |         playerImpl.setup(findViewById(android.R.id.content)); | ||||||
| @@ -147,7 +152,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         if (DEBUG) Log.d(TAG, "onResume() called"); |         if (DEBUG) Log.d(TAG, "onResume() called"); | ||||||
|         if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() |         if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() | ||||||
|                 && !playerImpl.isPlaying()) { |                 && !playerImpl.isPlaying()) { | ||||||
|             playerImpl.onVideoPlayPause(); |             playerImpl.onPlay(); | ||||||
|         } |         } | ||||||
|         activityPaused = false; |         activityPaused = false; | ||||||
|  |  | ||||||
| @@ -182,7 +187,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|  |  | ||||||
|         if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { |         if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { | ||||||
|             playerImpl.wasPlaying = playerImpl.isPlaying(); |             playerImpl.wasPlaying = playerImpl.isPlaying(); | ||||||
|             if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); |             playerImpl.onPause(); | ||||||
|         } |         } | ||||||
|         activityPaused = true; |         activityPaused = true; | ||||||
|     } |     } | ||||||
| @@ -337,6 +342,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Playback Parameters Listener | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { | ||||||
|  |         if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /////////////////////////////////////////////////////////////////////////// |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     @SuppressWarnings({"unused", "WeakerAccess"}) |     @SuppressWarnings({"unused", "WeakerAccess"}) | ||||||
| @@ -548,7 +562,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         public void onClick(View v) { |         public void onClick(View v) { | ||||||
|             super.onClick(v); |             super.onClick(v); | ||||||
|             if (v.getId() == playPauseButton.getId()) { |             if (v.getId() == playPauseButton.getId()) { | ||||||
|                 onVideoPlayPause(); |                 onPlayPause(); | ||||||
|  |  | ||||||
|             } else if (v.getId() == playPreviousButton.getId()) { |             } else if (v.getId() == playPreviousButton.getId()) { | ||||||
|                 onPlayPrevious(); |                 onPlayPrevious(); | ||||||
| @@ -597,28 +611,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|             updatePlaybackButtons(); |             updatePlaybackButtons(); | ||||||
|  |  | ||||||
|             getControlsRoot().setVisibility(View.INVISIBLE); |             getControlsRoot().setVisibility(View.INVISIBLE); | ||||||
|             queueLayout.setVisibility(View.VISIBLE); |             animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true, | ||||||
|  |                     DEFAULT_CONTROLS_DURATION); | ||||||
|  |  | ||||||
|             itemsList.scrollToPosition(playQueue.getIndex()); |             itemsList.scrollToPosition(playQueue.getIndex()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void onQueueClosed() { |         private void onQueueClosed() { | ||||||
|             queueLayout.setVisibility(View.GONE); |             animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false, | ||||||
|  |                     DEFAULT_CONTROLS_DURATION); | ||||||
|             queueVisible = false; |             queueVisible = false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void onMoreOptionsClicked() { |         private void onMoreOptionsClicked() { | ||||||
|             if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); |             if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); | ||||||
|  |  | ||||||
|             if (secondaryControls.getVisibility() == View.VISIBLE) { |             final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; | ||||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( |  | ||||||
|                         R.drawable.ic_expand_more_white_24dp)); |             animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, | ||||||
|                 animateView(secondaryControls, false, 200); |                     isMoreControlsVisible ? 0 : 180); | ||||||
|             } else { |             animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, | ||||||
|                 moreOptionsButton.setImageDrawable(getResources().getDrawable( |                     DEFAULT_CONTROLS_DURATION); | ||||||
|                         R.drawable.ic_expand_less_white_24dp)); |  | ||||||
|                 animateView(secondaryControls, true, 200); |  | ||||||
|             } |  | ||||||
|             showControls(DEFAULT_CONTROLS_DURATION); |             showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -628,6 +641,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|             showControlsThenHide(); |             showControlsThenHide(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public void onPlaybackSpeedClicked() { | ||||||
|  |             PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch()) | ||||||
|  |                     .show(getSupportFragmentManager(), TAG); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onStopTrackingTouch(SeekBar seekBar) { |         public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|             super.onStopTrackingTouch(seekBar); |             super.onStopTrackingTouch(seekBar); | ||||||
| @@ -638,6 +657,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         public void onDismiss(PopupMenu menu) { |         public void onDismiss(PopupMenu menu) { | ||||||
|             super.onDismiss(menu); |             super.onDismiss(menu); | ||||||
|             if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); |             if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); | ||||||
|  |             hideSystemUi(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
| @@ -696,7 +716,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|                 animatePlayButtons(true, 200); |                 animatePlayButtons(true, 200); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             changeSystemUi(); |  | ||||||
|             getRootView().setKeepScreenOn(true); |             getRootView().setKeepScreenOn(true); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -798,31 +817,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         private ItemTouchHelper.SimpleCallback getItemTouchCallback() { |         private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|             return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { |             return new PlayQueueItemTouchCallback() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { |                 public void onMove(int sourceIndex, int targetIndex) { | ||||||
|                     if (source.getItemViewType() != target.getItemViewType()) { |                     if (playQueue != null) playQueue.move(sourceIndex, targetIndex); | ||||||
|                         return false; |  | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                     final int sourceIndex = source.getLayoutPosition(); |  | ||||||
|                     final int targetIndex = target.getLayoutPosition(); |  | ||||||
|                     playQueue.move(sourceIndex, targetIndex); |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public boolean isLongPressDragEnabled() { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public boolean isItemViewSwipeEnabled() { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} |  | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -618,7 +618,7 @@ public final class PopupVideoPlayer extends Service { | |||||||
|                     onClose(); |                     onClose(); | ||||||
|                     break; |                     break; | ||||||
|                 case ACTION_PLAY_PAUSE: |                 case ACTION_PLAY_PAUSE: | ||||||
|                     onVideoPlayPause(); |                     onPlayPause(); | ||||||
|                     break; |                     break; | ||||||
|                 case ACTION_REPEAT: |                 case ACTION_REPEAT: | ||||||
|                     onRepeatClicked(); |                     onRepeatClicked(); | ||||||
| @@ -716,7 +716,7 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         public boolean onDoubleTap(MotionEvent e) { |         public boolean onDoubleTap(MotionEvent e) { | ||||||
|             if (DEBUG) |             if (DEBUG) | ||||||
|                 Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); |                 Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); | ||||||
|             if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false; |             if (playerImpl == null || !playerImpl.isPlaying()) return false; | ||||||
|  |  | ||||||
|             if (e.getX() > popupWidth / 2) { |             if (e.getX() > popupWidth / 2) { | ||||||
|                 playerImpl.onFastForward(); |                 playerImpl.onFastForward(); | ||||||
| @@ -731,7 +731,7 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         public boolean onSingleTapConfirmed(MotionEvent e) { |         public boolean onSingleTapConfirmed(MotionEvent e) { | ||||||
|             if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); |             if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); | ||||||
|             if (playerImpl == null || playerImpl.getPlayer() == null) return false; |             if (playerImpl == null || playerImpl.getPlayer() == null) return false; | ||||||
|             playerImpl.onVideoPlayPause(); |             playerImpl.onPlayPause(); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,9 +31,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; | |||||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||||
| import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; | import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; | ||||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | import org.schabi.newpipe.player.event.PlayerEventListener; | ||||||
|  | import org.schabi.newpipe.player.helper.PlaybackParameterDialog; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemHolder; | import org.schabi.newpipe.playlist.PlayQueueItemHolder; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| @@ -42,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch; | |||||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | ||||||
|  |  | ||||||
| public abstract class ServicePlayerActivity extends AppCompatActivity | public abstract class ServicePlayerActivity extends AppCompatActivity | ||||||
|         implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener { |         implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, | ||||||
|  |         View.OnClickListener, PlaybackParameterDialog.Callback { | ||||||
|  |  | ||||||
|     private boolean serviceBound; |     private boolean serviceBound; | ||||||
|     private ServiceConnection serviceConnection; |     private ServiceConnection serviceConnection; | ||||||
| @@ -56,14 +59,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|     //////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; |     private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; | ||||||
|     private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61; |  | ||||||
|     private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97; |  | ||||||
|  |  | ||||||
|     private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; |     private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; | ||||||
|  |  | ||||||
|     private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; |  | ||||||
|     private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; |  | ||||||
|  |  | ||||||
|     private View rootView; |     private View rootView; | ||||||
|  |  | ||||||
|     private RecyclerView itemsList; |     private RecyclerView itemsList; | ||||||
| @@ -87,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|     private ProgressBar progressBar; |     private ProgressBar progressBar; | ||||||
|  |  | ||||||
|     private TextView playbackSpeedButton; |     private TextView playbackSpeedButton; | ||||||
|     private PopupMenu playbackSpeedPopupMenu; |  | ||||||
|     private TextView playbackPitchButton; |     private TextView playbackPitchButton; | ||||||
|     private PopupMenu playbackPitchPopupMenu; |  | ||||||
|  |  | ||||||
|     //////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////// | ||||||
|     // Abstracts |     // Abstracts | ||||||
| @@ -319,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|         shuffleButton.setOnClickListener(this); |         shuffleButton.setOnClickListener(this); | ||||||
|         playbackSpeedButton.setOnClickListener(this); |         playbackSpeedButton.setOnClickListener(this); | ||||||
|         playbackPitchButton.setOnClickListener(this); |         playbackPitchButton.setOnClickListener(this); | ||||||
|  |  | ||||||
|         playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton); |  | ||||||
|         playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton); |  | ||||||
|         buildPlaybackSpeedMenu(); |  | ||||||
|         buildPlaybackPitchMenu(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void buildPlaybackSpeedMenu() { |  | ||||||
|         if (playbackSpeedPopupMenu == null) return; |  | ||||||
|  |  | ||||||
|         playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID); |  | ||||||
|         for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) { |  | ||||||
|             final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i]; |  | ||||||
|             final String formattedSpeed = formatSpeed(playbackSpeed); |  | ||||||
|             final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed); |  | ||||||
|             item.setOnMenuItemClickListener(menuItem -> { |  | ||||||
|                 if (player == null) return false; |  | ||||||
|  |  | ||||||
|                 player.setPlaybackSpeed(playbackSpeed); |  | ||||||
|                 return true; |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void buildPlaybackPitchMenu() { |  | ||||||
|         if (playbackPitchPopupMenu == null) return; |  | ||||||
|  |  | ||||||
|         playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID); |  | ||||||
|         for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) { |  | ||||||
|             final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i]; |  | ||||||
|             final String formattedPitch = formatPitch(playbackPitch); |  | ||||||
|             final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch); |  | ||||||
|             item.setOnMenuItemClickListener(menuItem -> { |  | ||||||
|                 if (player == null) return false; |  | ||||||
|  |  | ||||||
|                 player.setPlaybackPitch(playbackPitch); |  | ||||||
|                 return true; |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void buildItemPopupMenu(final PlayQueueItem item, final View view) { |     private void buildItemPopupMenu(final PlayQueueItem item, final View view) { | ||||||
| @@ -398,43 +355,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { |     private ItemTouchHelper.SimpleCallback getItemTouchCallback() { | ||||||
|         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { |         return new PlayQueueItemTouchCallback() { | ||||||
|             @Override |             @Override | ||||||
|             public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, |             public void onMove(int sourceIndex, int targetIndex) { | ||||||
|                                                     int viewSizeOutOfBounds, int totalSize, |  | ||||||
|                                                     long msSinceStartScroll) { |  | ||||||
|                 final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, |  | ||||||
|                         viewSizeOutOfBounds, totalSize, msSinceStartScroll); |  | ||||||
|                 final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, |  | ||||||
|                         Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); |  | ||||||
|                 return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, |  | ||||||
|                                   RecyclerView.ViewHolder target) { |  | ||||||
|                 if (source.getItemViewType() != target.getItemViewType()) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 final int sourceIndex = source.getLayoutPosition(); |  | ||||||
|                 final int targetIndex = target.getLayoutPosition(); |  | ||||||
|                 if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); |                 if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); | ||||||
|                 return true; |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean isLongPressDragEnabled() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public boolean isItemViewSwipeEnabled() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} |  | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -499,7 +424,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|             player.onPlayPrevious(); |             player.onPlayPrevious(); | ||||||
|  |  | ||||||
|         } else if (view.getId() == playPauseButton.getId()) { |         } else if (view.getId() == playPauseButton.getId()) { | ||||||
|             player.onVideoPlayPause(); |             player.onPlayPause(); | ||||||
|  |  | ||||||
|         } else if (view.getId() == forwardButton.getId()) { |         } else if (view.getId() == forwardButton.getId()) { | ||||||
|             player.onPlayNext(); |             player.onPlayNext(); | ||||||
| @@ -508,10 +433,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|             player.onShuffleClicked(); |             player.onShuffleClicked(); | ||||||
|  |  | ||||||
|         } else if (view.getId() == playbackSpeedButton.getId()) { |         } else if (view.getId() == playbackSpeedButton.getId()) { | ||||||
|             playbackSpeedPopupMenu.show(); |             openPlaybackParameterDialog(); | ||||||
|  |  | ||||||
|         } else if (view.getId() == playbackPitchButton.getId()) { |         } else if (view.getId() == playbackPitchButton.getId()) { | ||||||
|             playbackPitchPopupMenu.show(); |             openPlaybackParameterDialog(); | ||||||
|  |  | ||||||
|         } else if (view.getId() == metadata.getId()) { |         } else if (view.getId() == metadata.getId()) { | ||||||
|             scrollToSelected(); |             scrollToSelected(); | ||||||
| @@ -522,6 +447,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Playback Parameters | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     private void openPlaybackParameterDialog() { | ||||||
|  |         if (player == null) return; | ||||||
|  |         PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), | ||||||
|  |                 player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) { | ||||||
|  |         if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     //////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////// | ||||||
|     // Seekbar Listener |     // Seekbar Listener | ||||||
|     //////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -543,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onStopTrackingTouch(SeekBar seekBar) { |     public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|         if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress()); |         if (player != null) player.seekTo(seekBar.getProgress()); | ||||||
|         seekDisplay.setVisibility(View.GONE); |         seekDisplay.setVisibility(View.GONE); | ||||||
|         seeking = false; |         seeking = false; | ||||||
|     } |     } | ||||||
| @@ -573,6 +513,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|             progressSeekBar.setProgress(currentProgress); |             progressSeekBar.setProgress(currentProgress); | ||||||
|             progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); |             progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (player != null) { | ||||||
|  |             progressLiveSync.setClickable(!player.isLiveEdge()); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ import android.widget.TextView; | |||||||
|  |  | ||||||
| import com.google.android.exoplayer2.C; | import com.google.android.exoplayer2.C; | ||||||
| import com.google.android.exoplayer2.Format; | import com.google.android.exoplayer2.Format; | ||||||
|  | import com.google.android.exoplayer2.PlaybackParameters; | ||||||
| import com.google.android.exoplayer2.Player; | import com.google.android.exoplayer2.Player; | ||||||
| import com.google.android.exoplayer2.source.MediaSource; | import com.google.android.exoplayer2.source.MediaSource; | ||||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | import com.google.android.exoplayer2.source.MergingMediaSource; | ||||||
| @@ -425,7 +426,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         // Create subtitle sources |         // Create subtitle sources | ||||||
|         for (final Subtitles subtitle : info.getSubtitles()) { |         for (final Subtitles subtitle : info.getSubtitles()) { | ||||||
|             final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); |             final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); | ||||||
|             if (mimeType == null || context == null) continue; |             if (mimeType == null) continue; | ||||||
|  |  | ||||||
|             final Format textFormat = Format.createTextSampleFormat(null, mimeType, |             final Format textFormat = Format.createTextSampleFormat(null, mimeType, | ||||||
|                     SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); |                     SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); | ||||||
| @@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         onTextTrackUpdate(); |         onTextTrackUpdate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { | ||||||
|  |         super.onPlaybackParametersChanged(playbackParameters); | ||||||
|  |         playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { |     public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
| @@ -599,7 +606,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { |     public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { | ||||||
|         if (!isPrepared) return; |         if (!isPrepared()) return; | ||||||
|  |  | ||||||
|         if (duration != playbackSeekBar.getMax()) { |         if (duration != playbackSeekBar.getMax()) { | ||||||
|             playbackEndTime.setText(getTimeString(duration)); |             playbackEndTime.setText(getTimeString(duration)); | ||||||
| @@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         if (DEBUG && bufferPercent % 20 == 0) { //Limit log |         if (DEBUG && bufferPercent % 20 == 0) { //Limit log | ||||||
|             Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); |             Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); | ||||||
|         } |         } | ||||||
|  |         playbackLiveSync.setClickable(!isLiveEdge()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -624,8 +632,6 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void onFullScreenButtonClicked() { |     protected void onFullScreenButtonClicked() { | ||||||
|         if (!isPlayerReady()) return; |  | ||||||
|  |  | ||||||
|         changeState(STATE_BLOCKED); |         changeState(STATE_BLOCKED); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -720,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         wasPlaying = simpleExoPlayer.getPlayWhenReady(); |         wasPlaying = simpleExoPlayer.getPlayWhenReady(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onPlaybackSpeedClicked() { |     public void onPlaybackSpeedClicked() { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); |         if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); | ||||||
|         playbackSpeedPopupMenu.show(); |         playbackSpeedPopupMenu.show(); | ||||||
|         isSomePopupMenuVisible = true; |         isSomePopupMenuVisible = true; | ||||||
| @@ -735,7 +741,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onResizeClicked() { |     private void onResizeClicked() { | ||||||
|         if (getAspectRatioFrameLayout() != null && context != null) { |         if (getAspectRatioFrameLayout() != null) { | ||||||
|             final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); |             final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); | ||||||
|             final int newResizeMode = nextResizeMode(currentResizeMode); |             final int newResizeMode = nextResizeMode(currentResizeMode); | ||||||
|             getAspectRatioFrameLayout().setResizeMode(newResizeMode); |             getAspectRatioFrameLayout().setResizeMode(newResizeMode); | ||||||
| @@ -772,7 +778,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     public void onStopTrackingTouch(SeekBar seekBar) { |     public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|         if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); |         if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); | ||||||
|  |  | ||||||
|         simpleExoPlayer.seekTo(seekBar.getProgress()); |         seekTo(seekBar.getProgress()); | ||||||
|         if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); |         if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); | ||||||
|  |  | ||||||
|         playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); |         playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ package org.schabi.newpipe.player.helper; | |||||||
| import android.animation.Animator; | import android.animation.Animator; | ||||||
| import android.animation.AnimatorListenerAdapter; | import android.animation.AnimatorListenerAdapter; | ||||||
| import android.animation.ValueAnimator; | import android.animation.ValueAnimator; | ||||||
| import android.content.ComponentName; |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.media.AudioFocusRequest; | import android.media.AudioFocusRequest; | ||||||
| @@ -18,10 +17,14 @@ import com.google.android.exoplayer2.SimpleExoPlayer; | |||||||
| import com.google.android.exoplayer2.audio.AudioRendererEventListener; | import com.google.android.exoplayer2.audio.AudioRendererEventListener; | ||||||
| import com.google.android.exoplayer2.decoder.DecoderCounters; | import com.google.android.exoplayer2.decoder.DecoderCounters; | ||||||
|  |  | ||||||
| public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener { | public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, | ||||||
|  |         AudioRendererEventListener { | ||||||
|  |  | ||||||
|     private static final String TAG = "AudioFocusReactor"; |     private static final String TAG = "AudioFocusReactor"; | ||||||
|  |  | ||||||
|  |     private static final boolean SHOULD_BUILD_FOCUS_REQUEST = | ||||||
|  |             Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; | ||||||
|  |  | ||||||
|     private static final int DUCK_DURATION = 1500; |     private static final int DUCK_DURATION = 1500; | ||||||
|     private static final float DUCK_AUDIO_TO = .2f; |     private static final float DUCK_AUDIO_TO = .2f; | ||||||
|  |  | ||||||
| @@ -34,13 +37,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au | |||||||
|  |  | ||||||
|     private final AudioFocusRequest request; |     private final AudioFocusRequest request; | ||||||
|  |  | ||||||
|     public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { |     public AudioReactor(@NonNull final Context context, | ||||||
|  |                         @NonNull final SimpleExoPlayer player) { | ||||||
|         this.player = player; |         this.player = player; | ||||||
|         this.context = context; |         this.context = context; | ||||||
|         this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |         this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); | ||||||
|         player.setAudioDebugListener(this); |         player.addAudioDebugListener(this); | ||||||
|  |  | ||||||
|         if (shouldBuildFocusRequest()) { |         if (SHOULD_BUILD_FOCUS_REQUEST) { | ||||||
|             request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) |             request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) | ||||||
|                     .setAcceptsDelayedFocusGain(true) |                     .setAcceptsDelayedFocusGain(true) | ||||||
|                     .setWillPauseWhenDucked(true) |                     .setWillPauseWhenDucked(true) | ||||||
| @@ -51,12 +55,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void dispose() { | ||||||
|  |         abandonAudioFocus(); | ||||||
|  |         player.removeAudioDebugListener(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Audio Manager |     // Audio Manager | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     public void requestAudioFocus() { |     public void requestAudioFocus() { | ||||||
|         if (shouldBuildFocusRequest()) { |         if (SHOULD_BUILD_FOCUS_REQUEST) { | ||||||
|             audioManager.requestAudioFocus(request); |             audioManager.requestAudioFocus(request); | ||||||
|         } else { |         } else { | ||||||
|             audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); |             audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); | ||||||
| @@ -64,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void abandonAudioFocus() { |     public void abandonAudioFocus() { | ||||||
|         if (shouldBuildFocusRequest()) { |         if (SHOULD_BUILD_FOCUS_REQUEST) { | ||||||
|             audioManager.abandonAudioFocusRequest(request); |             audioManager.abandonAudioFocusRequest(request); | ||||||
|         } else { |         } else { | ||||||
|             audioManager.abandonAudioFocus(this); |             audioManager.abandonAudioFocus(this); | ||||||
| @@ -83,26 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au | |||||||
|         audioManager.setStreamVolume(STREAM_TYPE, volume, 0); |         audioManager.setStreamVolume(STREAM_TYPE, volume, 0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean shouldBuildFocusRequest() { |  | ||||||
|         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void registerMediaButtonEventReceiver(ComponentName componentName) { |  | ||||||
|         if (android.os.Build.VERSION.SDK_INT > 27) { |  | ||||||
|             Log.e(TAG, "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         audioManager.registerMediaButtonEventReceiver(componentName); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void unregisterMediaButtonEventReceiver(ComponentName componentName) { |  | ||||||
|         if (android.os.Build.VERSION.SDK_INT > 27) { |  | ||||||
|             Log.e(TAG, "unregisterMediaButtonEventReceiver has been deprecated and maybe not supported anymore."); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         audioManager.unregisterMediaButtonEventReceiver(componentName); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // AudioFocus |     // AudioFocus | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -165,12 +154,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au | |||||||
|                 player.setVolume(to); |                 player.setVolume(to); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |         valueAnimator.addUpdateListener(animation -> | ||||||
|             @Override |                 player.setVolume(((float) animation.getAnimatedValue()))); | ||||||
|             public void onAnimationUpdate(ValueAnimator animation) { |  | ||||||
|                 player.setVolume(((float) animation.getAnimatedValue())); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         valueAnimator.start(); |         valueAnimator.start(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | package org.schabi.newpipe.player.helper; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.v4.media.session.MediaSessionCompat; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.Player; | ||||||
|  | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer; | ||||||
|  | import org.schabi.newpipe.player.mediasession.MediaSessionCallback; | ||||||
|  | import org.schabi.newpipe.player.mediasession.PlayQueueNavigator; | ||||||
|  | import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController; | ||||||
|  |  | ||||||
|  | public class MediaSessionManager { | ||||||
|  |     private static final String TAG = "MediaSessionManager"; | ||||||
|  |  | ||||||
|  |     private final MediaSessionCompat mediaSession; | ||||||
|  |     private final MediaSessionConnector sessionConnector; | ||||||
|  |  | ||||||
|  |     public MediaSessionManager(@NonNull final Context context, | ||||||
|  |                                @NonNull final Player player, | ||||||
|  |                                @NonNull final MediaSessionCallback callback) { | ||||||
|  |         this.mediaSession = new MediaSessionCompat(context, TAG); | ||||||
|  |         this.sessionConnector = new MediaSessionConnector(mediaSession, | ||||||
|  |                 new PlayQueuePlaybackController(callback)); | ||||||
|  |         this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); | ||||||
|  |         this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public MediaSessionCompat getMediaSession() { | ||||||
|  |         return mediaSession; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public MediaSessionConnector getSessionConnector() { | ||||||
|  |         return sessionConnector; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,379 @@ | |||||||
|  | package org.schabi.newpipe.player.helper; | ||||||
|  |  | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v4.app.DialogFragment; | ||||||
|  | import android.support.v7.app.AlertDialog; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.CheckBox; | ||||||
|  | import android.widget.SeekBar; | ||||||
|  | import android.widget.TextView; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.util.SliderStrategy; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.player.BasePlayer.DEBUG; | ||||||
|  |  | ||||||
|  | public class PlaybackParameterDialog extends DialogFragment { | ||||||
|  |     @NonNull private static final String TAG = "PlaybackParameterDialog"; | ||||||
|  |  | ||||||
|  |     public static final double MINIMUM_PLAYBACK_VALUE = 0.25f; | ||||||
|  |     public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; | ||||||
|  |  | ||||||
|  |     public static final char STEP_UP_SIGN = '+'; | ||||||
|  |     public static final char STEP_DOWN_SIGN = '-'; | ||||||
|  |     public static final double PLAYBACK_STEP_VALUE = 0.05f; | ||||||
|  |  | ||||||
|  |     public static final double NIGHTCORE_TEMPO = 1.20f; | ||||||
|  |     public static final double NIGHTCORE_PITCH_LOWER = 1.15f; | ||||||
|  |     public static final double NIGHTCORE_PITCH_UPPER = 1.25f; | ||||||
|  |  | ||||||
|  |     public static final double DEFAULT_TEMPO = 1.00f; | ||||||
|  |     public static final double DEFAULT_PITCH = 1.00f; | ||||||
|  |  | ||||||
|  |     @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; | ||||||
|  |     @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; | ||||||
|  |  | ||||||
|  |     public interface Callback { | ||||||
|  |         void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable private Callback callback; | ||||||
|  |  | ||||||
|  |     @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic( | ||||||
|  |             MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE, | ||||||
|  |             /*centerAt=*/1.00f, /*sliderGranularity=*/10000); | ||||||
|  |  | ||||||
|  |     private double initialTempo = DEFAULT_TEMPO; | ||||||
|  |     private double initialPitch = DEFAULT_PITCH; | ||||||
|  |  | ||||||
|  |     @Nullable private SeekBar tempoSlider; | ||||||
|  |     @Nullable private TextView tempoMinimumText; | ||||||
|  |     @Nullable private TextView tempoMaximumText; | ||||||
|  |     @Nullable private TextView tempoCurrentText; | ||||||
|  |     @Nullable private TextView tempoStepDownText; | ||||||
|  |     @Nullable private TextView tempoStepUpText; | ||||||
|  |  | ||||||
|  |     @Nullable private SeekBar pitchSlider; | ||||||
|  |     @Nullable private TextView pitchMinimumText; | ||||||
|  |     @Nullable private TextView pitchMaximumText; | ||||||
|  |     @Nullable private TextView pitchCurrentText; | ||||||
|  |     @Nullable private TextView pitchStepDownText; | ||||||
|  |     @Nullable private TextView pitchStepUpText; | ||||||
|  |  | ||||||
|  |     @Nullable private CheckBox unhookingCheckbox; | ||||||
|  |  | ||||||
|  |     @Nullable private TextView nightCorePresetText; | ||||||
|  |     @Nullable private TextView resetPresetText; | ||||||
|  |  | ||||||
|  |     public static PlaybackParameterDialog newInstance(final double playbackTempo, | ||||||
|  |                                                       final double playbackPitch) { | ||||||
|  |         PlaybackParameterDialog dialog = new PlaybackParameterDialog(); | ||||||
|  |         dialog.initialTempo = playbackTempo; | ||||||
|  |         dialog.initialPitch = playbackPitch; | ||||||
|  |         return dialog; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Lifecycle | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  |         if (context != null && context instanceof Callback) { | ||||||
|  |             callback = (Callback) context; | ||||||
|  |         } else { | ||||||
|  |             dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         if (savedInstanceState != null) { | ||||||
|  |             initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); | ||||||
|  |             initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSaveInstanceState(Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |         outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); | ||||||
|  |         outState.putDouble(INITIAL_PITCH_KEY, initialPitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Dialog | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { | ||||||
|  |         final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); | ||||||
|  |         setupControlViews(view); | ||||||
|  |  | ||||||
|  |         final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) | ||||||
|  |                 .setTitle(R.string.playback_speed_control) | ||||||
|  |                 .setView(view) | ||||||
|  |                 .setCancelable(true) | ||||||
|  |                 .setNegativeButton(R.string.cancel, (dialogInterface, i) -> | ||||||
|  |                         setPlaybackParameters(initialTempo, initialPitch)) | ||||||
|  |                 .setPositiveButton(R.string.finish, (dialogInterface, i) -> | ||||||
|  |                         setCurrentPlaybackParameters()); | ||||||
|  |  | ||||||
|  |         return dialogBuilder.create(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Control Views | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private void setupControlViews(@NonNull View rootView) { | ||||||
|  |         setupHookingControl(rootView); | ||||||
|  |         setupTempoControl(rootView); | ||||||
|  |         setupPitchControl(rootView); | ||||||
|  |         setupPresetControl(rootView); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setupTempoControl(@NonNull View rootView) { | ||||||
|  |         tempoSlider = rootView.findViewById(R.id.tempoSeekbar); | ||||||
|  |         tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); | ||||||
|  |         tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); | ||||||
|  |         tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText); | ||||||
|  |         tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); | ||||||
|  |         tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); | ||||||
|  |  | ||||||
|  |         if (tempoCurrentText != null) | ||||||
|  |             tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); | ||||||
|  |         if (tempoMaximumText != null) | ||||||
|  |             tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); | ||||||
|  |         if (tempoMinimumText != null) | ||||||
|  |             tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); | ||||||
|  |  | ||||||
|  |         if (tempoStepUpText != null) { | ||||||
|  |             tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); | ||||||
|  |             tempoStepUpText.setOnClickListener(view -> { | ||||||
|  |                 onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (tempoStepDownText != null) { | ||||||
|  |             tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); | ||||||
|  |             tempoStepDownText.setOnClickListener(view -> { | ||||||
|  |                 onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (tempoSlider != null) { | ||||||
|  |             tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); | ||||||
|  |             tempoSlider.setProgress(strategy.progressOf(initialTempo)); | ||||||
|  |             tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setupPitchControl(@NonNull View rootView) { | ||||||
|  |         pitchSlider = rootView.findViewById(R.id.pitchSeekbar); | ||||||
|  |         pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); | ||||||
|  |         pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); | ||||||
|  |         pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); | ||||||
|  |         pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); | ||||||
|  |         pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); | ||||||
|  |  | ||||||
|  |         if (pitchCurrentText != null) | ||||||
|  |             pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); | ||||||
|  |         if (pitchMaximumText != null) | ||||||
|  |             pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); | ||||||
|  |         if (pitchMinimumText != null) | ||||||
|  |             pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); | ||||||
|  |  | ||||||
|  |         if (pitchStepUpText != null) { | ||||||
|  |             pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); | ||||||
|  |             pitchStepUpText.setOnClickListener(view -> { | ||||||
|  |                 onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (pitchStepDownText != null) { | ||||||
|  |             pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); | ||||||
|  |             pitchStepDownText.setOnClickListener(view -> { | ||||||
|  |                 onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (pitchSlider != null) { | ||||||
|  |             pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); | ||||||
|  |             pitchSlider.setProgress(strategy.progressOf(initialPitch)); | ||||||
|  |             pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setupHookingControl(@NonNull View rootView) { | ||||||
|  |         unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); | ||||||
|  |         if (unhookingCheckbox != null) { | ||||||
|  |             unhookingCheckbox.setChecked(initialPitch != initialTempo); | ||||||
|  |             unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { | ||||||
|  |                 if (isChecked) return; | ||||||
|  |                 // When unchecked, slide back to the minimum of current tempo or pitch | ||||||
|  |                 final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); | ||||||
|  |                 setSliders(minimum); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setupPresetControl(@NonNull View rootView) { | ||||||
|  |         nightCorePresetText = rootView.findViewById(R.id.presetNightcore); | ||||||
|  |         if (nightCorePresetText != null) { | ||||||
|  |             nightCorePresetText.setOnClickListener(view -> { | ||||||
|  |                 final double randomPitch = NIGHTCORE_PITCH_LOWER + | ||||||
|  |                         Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER); | ||||||
|  |  | ||||||
|  |                 setTempoSlider(NIGHTCORE_TEMPO); | ||||||
|  |                 setPitchSlider(randomPitch); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         resetPresetText = rootView.findViewById(R.id.presetReset); | ||||||
|  |         if (resetPresetText != null) { | ||||||
|  |             resetPresetText.setOnClickListener(view -> { | ||||||
|  |                 setTempoSlider(DEFAULT_TEMPO); | ||||||
|  |                 setPitchSlider(DEFAULT_PITCH); | ||||||
|  |                 setCurrentPlaybackParameters(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Sliders | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { | ||||||
|  |         return new SeekBar.OnSeekBarChangeListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { | ||||||
|  |                 final double currentTempo = strategy.valueOf(progress); | ||||||
|  |                 if (fromUser) { | ||||||
|  |                     onTempoSliderUpdated(currentTempo); | ||||||
|  |                     setCurrentPlaybackParameters(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onStartTrackingTouch(SeekBar seekBar) { | ||||||
|  |                 // Do Nothing. | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|  |                 // Do Nothing. | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { | ||||||
|  |         return new SeekBar.OnSeekBarChangeListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { | ||||||
|  |                 final double currentPitch = strategy.valueOf(progress); | ||||||
|  |                 if (fromUser) { // this change is first in chain | ||||||
|  |                     onPitchSliderUpdated(currentPitch); | ||||||
|  |                     setCurrentPlaybackParameters(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onStartTrackingTouch(SeekBar seekBar) { | ||||||
|  |                 // Do Nothing. | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|  |                 // Do Nothing. | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onTempoSliderUpdated(final double newTempo) { | ||||||
|  |         if (unhookingCheckbox == null) return; | ||||||
|  |         if (!unhookingCheckbox.isChecked()) { | ||||||
|  |             setSliders(newTempo); | ||||||
|  |         } else { | ||||||
|  |             setTempoSlider(newTempo); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onPitchSliderUpdated(final double newPitch) { | ||||||
|  |         if (unhookingCheckbox == null) return; | ||||||
|  |         if (!unhookingCheckbox.isChecked()) { | ||||||
|  |             setSliders(newPitch); | ||||||
|  |         } else { | ||||||
|  |             setPitchSlider(newPitch); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setSliders(final double newValue) { | ||||||
|  |         setTempoSlider(newValue); | ||||||
|  |         setPitchSlider(newValue); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setTempoSlider(final double newTempo) { | ||||||
|  |         if (tempoSlider == null) return; | ||||||
|  |         tempoSlider.setProgress(strategy.progressOf(newTempo)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setPitchSlider(final double newPitch) { | ||||||
|  |         if (pitchSlider == null) return; | ||||||
|  |         pitchSlider.setProgress(strategy.progressOf(newPitch)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Helper | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private void setCurrentPlaybackParameters() { | ||||||
|  |         setPlaybackParameters(getCurrentTempo(), getCurrentPitch()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setPlaybackParameters(final double tempo, final double pitch) { | ||||||
|  |         if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "Setting playback parameters to " + | ||||||
|  |                     "tempo=[" + tempo + "], " + | ||||||
|  |                     "pitch=[" + pitch + "]"); | ||||||
|  |  | ||||||
|  |             tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); | ||||||
|  |             pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); | ||||||
|  |             callback.onPlaybackParameterChanged((float) tempo, (float) pitch); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private double getCurrentTempo() { | ||||||
|  |         return tempoSlider == null ? initialTempo : strategy.valueOf( | ||||||
|  |                 tempoSlider.getProgress()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private double getCurrentPitch() { | ||||||
|  |         return pitchSlider == null ? initialPitch : strategy.valueOf( | ||||||
|  |                 pitchSlider.getProgress()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     private static String getStepUpPercentString(final double percent) { | ||||||
|  |         return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     private static String getStepDownPercentString(final double percent) { | ||||||
|  |         return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -60,11 +60,11 @@ public class PlayerHelper { | |||||||
|                 : stringFormatter.format("%02d:%02d", minutes, seconds).toString(); |                 : stringFormatter.format("%02d:%02d", minutes, seconds).toString(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String formatSpeed(float speed) { |     public static String formatSpeed(double speed) { | ||||||
|         return speedFormatter.format(speed); |         return speedFormatter.format(speed); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String formatPitch(float pitch) { |     public static String formatPitch(double pitch) { | ||||||
|         return pitchFormatter.format(pitch); |         return pitchFormatter.format(pitch); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasession; | ||||||
|  |  | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.ResultReceiver; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.Player; | ||||||
|  | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; | ||||||
|  |  | ||||||
|  | public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { | ||||||
|  |     @Override | ||||||
|  |     public long getSupportedPrepareActions() { | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPrepare() { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPrepareFromMediaId(String mediaId, Bundle extras) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPrepareFromSearch(String query, Bundle extras) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPrepareFromUri(Uri uri, Bundle extras) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String[] getCommands() { | ||||||
|  |         return new String[0]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasession; | ||||||
|  |  | ||||||
|  | import android.support.v4.media.MediaDescriptionCompat; | ||||||
|  |  | ||||||
|  | public interface MediaSessionCallback { | ||||||
|  |     void onSkipToPrevious(); | ||||||
|  |     void onSkipToNext(); | ||||||
|  |     void onSkipToIndex(final int index); | ||||||
|  |  | ||||||
|  |     int getCurrentPlayingIndex(); | ||||||
|  |     int getQueueSize(); | ||||||
|  |     MediaDescriptionCompat getQueueMetadata(final int index); | ||||||
|  |  | ||||||
|  |     void onPlay(); | ||||||
|  |     void onPause(); | ||||||
|  |     void onSetShuffle(final boolean isShuffled); | ||||||
|  | } | ||||||
| @@ -0,0 +1,111 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasession; | ||||||
|  |  | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.ResultReceiver; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v4.media.session.MediaSessionCompat; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.Player; | ||||||
|  | import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; | ||||||
|  | import com.google.android.exoplayer2.util.Util; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; | ||||||
|  | import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; | ||||||
|  | import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { | ||||||
|  |     public static final int DEFAULT_MAX_QUEUE_SIZE = 10; | ||||||
|  |  | ||||||
|  |     private final MediaSessionCompat mediaSession; | ||||||
|  |     private final MediaSessionCallback callback; | ||||||
|  |     private final int maxQueueSize; | ||||||
|  |  | ||||||
|  |     private long activeQueueItemId; | ||||||
|  |  | ||||||
|  |     public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, | ||||||
|  |                               @NonNull final MediaSessionCallback callback) { | ||||||
|  |         this.mediaSession = mediaSession; | ||||||
|  |         this.callback = callback; | ||||||
|  |         this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; | ||||||
|  |  | ||||||
|  |         this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long getSupportedQueueNavigatorActions(@Nullable Player player) { | ||||||
|  |         return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onTimelineChanged(Player player) { | ||||||
|  |         publishFloatingQueueWindow(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCurrentWindowIndexChanged(Player player) { | ||||||
|  |         if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID | ||||||
|  |                 || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { | ||||||
|  |             publishFloatingQueueWindow(); | ||||||
|  |         } else if (!player.getCurrentTimeline().isEmpty()) { | ||||||
|  |             activeQueueItemId = player.getCurrentWindowIndex(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long getActiveQueueItemId(@Nullable Player player) { | ||||||
|  |         return callback.getCurrentPlayingIndex(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToPrevious(Player player) { | ||||||
|  |         callback.onSkipToPrevious(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToQueueItem(Player player, long id) { | ||||||
|  |         callback.onSkipToIndex((int) id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToNext(Player player) { | ||||||
|  |         callback.onSkipToNext(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void publishFloatingQueueWindow() { | ||||||
|  |         if (callback.getQueueSize() == 0) { | ||||||
|  |             mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList()); | ||||||
|  |             activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Yes this is almost a copypasta, got a problem with that? =\ | ||||||
|  |         int windowCount = callback.getQueueSize(); | ||||||
|  |         int currentWindowIndex = callback.getCurrentPlayingIndex(); | ||||||
|  |         int queueSize = Math.min(maxQueueSize, windowCount); | ||||||
|  |         int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, | ||||||
|  |                 windowCount - queueSize); | ||||||
|  |  | ||||||
|  |         List<MediaSessionCompat.QueueItem> queue = new ArrayList<>(); | ||||||
|  |         for (int i = startIndex; i < startIndex + queueSize; i++) { | ||||||
|  |             queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); | ||||||
|  |         } | ||||||
|  |         mediaSession.setQueue(queue); | ||||||
|  |         activeQueueItemId = currentWindowIndex; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String[] getCommands() { | ||||||
|  |         return new String[0]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,31 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasession; | ||||||
|  |  | ||||||
|  | import android.support.v4.media.session.PlaybackStateCompat; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.Player; | ||||||
|  | import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController; | ||||||
|  |  | ||||||
|  | public class PlayQueuePlaybackController extends DefaultPlaybackController { | ||||||
|  |     private final MediaSessionCallback callback; | ||||||
|  |  | ||||||
|  |     public PlayQueuePlaybackController(final MediaSessionCallback callback) { | ||||||
|  |         super(); | ||||||
|  |         this.callback = callback; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlay(Player player) { | ||||||
|  |         callback.onPlay(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPause(Player player) { | ||||||
|  |         callback.onPause(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSetShuffleMode(Player player, int shuffleMode) { | ||||||
|  |         callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL | ||||||
|  |                 || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -72,7 +72,13 @@ public class FailedMediaSource implements ManagedMediaSource { | |||||||
|     public void releaseSource() {} |     public void releaseSource() {} | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { |     public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, | ||||||
|  |                                         final boolean isInterruptable) { | ||||||
|         return newIdentity != playQueueItem || canRetry(); |         return newIdentity != playQueueItem || canRetry(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isStreamEqual(@NonNull PlayQueueItem stream) { | ||||||
|  |         return playQueueItem == stream; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -59,7 +59,13 @@ public class LoadedMediaSource implements ManagedMediaSource { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { |     public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, | ||||||
|         return newIdentity != stream || isExpired(); |                                         final boolean isInterruptable) { | ||||||
|  |         return newIdentity != stream || (isInterruptable && isExpired()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isStreamEqual(@NonNull PlayQueueItem stream) { | ||||||
|  |         return this.stream == stream; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,5 +7,21 @@ import com.google.android.exoplayer2.source.MediaSource; | |||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
| public interface ManagedMediaSource extends MediaSource { | public interface ManagedMediaSource extends MediaSource { | ||||||
|     boolean canReplace(@NonNull final PlayQueueItem newIdentity); |     /** | ||||||
|  |      * Determines whether or not this {@link ManagedMediaSource} can be replaced. | ||||||
|  |      * | ||||||
|  |      * @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if | ||||||
|  |      *                    it is different from the existing stream in the | ||||||
|  |      *                    {@link ManagedMediaSource}, then it should be replaced. | ||||||
|  |      * @param isInterruptable specifies if this {@link ManagedMediaSource} potentially | ||||||
|  |      *                        being played. | ||||||
|  |      * */ | ||||||
|  |     boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity, | ||||||
|  |                                  final boolean isInterruptable); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Determines if the {@link PlayQueueItem} is the one the | ||||||
|  |      * {@link ManagedMediaSource} encapsulates over. | ||||||
|  |      * */ | ||||||
|  |     boolean isStreamEqual(@NonNull final PlayQueueItem stream); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,7 +19,13 @@ public class PlaceholderMediaSource implements ManagedMediaSource { | |||||||
|     @Override public void releaseSource() {} |     @Override public void releaseSource() {} | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { |     public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, | ||||||
|  |                                         final boolean isInterruptable) { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isStreamEqual(@NonNull PlayQueueItem stream) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | package org.schabi.newpipe.player.playback; | ||||||
|  |  | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.support.v4.media.MediaDescriptionCompat; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.player.BasePlayer; | ||||||
|  | import org.schabi.newpipe.player.mediasession.MediaSessionCallback; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
|  | public class BasePlayerMediaSession implements MediaSessionCallback { | ||||||
|  |     private BasePlayer player; | ||||||
|  |  | ||||||
|  |     public BasePlayerMediaSession(final BasePlayer player) { | ||||||
|  |         this.player = player; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToPrevious() { | ||||||
|  |         player.onPlayPrevious(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToNext() { | ||||||
|  |         player.onPlayNext(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSkipToIndex(int index) { | ||||||
|  |         if (player.getPlayQueue() == null) return; | ||||||
|  |         player.onSelected(player.getPlayQueue().getItem(index)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int getCurrentPlayingIndex() { | ||||||
|  |         if (player.getPlayQueue() == null) return -1; | ||||||
|  |         return player.getPlayQueue().getIndex(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int getQueueSize() { | ||||||
|  |         if (player.getPlayQueue() == null) return -1; | ||||||
|  |         return player.getPlayQueue().size(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public MediaDescriptionCompat getQueueMetadata(int index) { | ||||||
|  |         if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final PlayQueueItem item = player.getPlayQueue().getItem(index); | ||||||
|  |         MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() | ||||||
|  |                 .setMediaId(String.valueOf(index)) | ||||||
|  |                 .setTitle(item.getTitle()) | ||||||
|  |                 .setSubtitle(item.getUploader()); | ||||||
|  |  | ||||||
|  |         final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl()); | ||||||
|  |         if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri); | ||||||
|  |  | ||||||
|  |         return descriptionBuilder.build(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlay() { | ||||||
|  |         player.onPlay(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPause() { | ||||||
|  |         player.onPause(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSetShuffle(boolean isShuffled) { | ||||||
|  |         player.onShuffleModeEnabledChanged(isShuffled); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent; | |||||||
| import org.schabi.newpipe.playlist.events.PlayQueueEvent; | import org.schabi.newpipe.playlist.events.PlayQueueEvent; | ||||||
| import org.schabi.newpipe.playlist.events.RemoveEvent; | import org.schabi.newpipe.playlist.events.RemoveEvent; | ||||||
| import org.schabi.newpipe.playlist.events.ReorderEvent; | import org.schabi.newpipe.playlist.events.ReorderEvent; | ||||||
|  | import org.schabi.newpipe.util.ServiceHelper; | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; |  | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
|  | import io.reactivex.Observable; | ||||||
| import io.reactivex.Single; | import io.reactivex.Single; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
| @@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject; | |||||||
| import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | ||||||
|  |  | ||||||
| public class MediaSourceManager { | public class MediaSourceManager { | ||||||
|     @NonNull private final static String TAG = "MediaSourceManager"; |     @NonNull private final String TAG = "MediaSourceManager@" + hashCode(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Determines how many streams before and after the current stream should be loaded. |      * Determines how many streams before and after the current stream should be loaded. | ||||||
| @@ -60,17 +60,18 @@ public class MediaSourceManager { | |||||||
|     @NonNull private final PlayQueue playQueue; |     @NonNull private final PlayQueue playQueue; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing |      * Determines the gap time between the playback position and the playback duration which | ||||||
|      * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure |      * the {@link #getEdgeIntervalSignal()} begins to request loading. | ||||||
|      * the {@link StreamInfo} used in subsequent playback is up-to-date. |  | ||||||
|      * <br><br> |  | ||||||
|      * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to |  | ||||||
|      * replace the expired one on whereupon {@link #loadImmediate()} is called. |  | ||||||
|      * |      * | ||||||
|      * @see #loadImmediate() |      * @see #progressUpdateIntervalMillis | ||||||
|      * @see #isCorrectionNeeded(PlayQueueItem) |  | ||||||
|      * */ |      * */ | ||||||
|     private final long windowRefreshTimeMillis; |     private final long playbackNearEndGapMillis; | ||||||
|  |     /** | ||||||
|  |      * Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between | ||||||
|  |      * each request for loading, once {@link #playbackNearEndGapMillis} has reached. | ||||||
|  |      * */ | ||||||
|  |     private final long progressUpdateIntervalMillis; | ||||||
|  |     @NonNull private final Observable<Long> nearEndIntervalSignal; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Process only the last load order when receiving a stream of load orders (lessens I/O). |      * Process only the last load order when receiving a stream of load orders (lessens I/O). | ||||||
| @@ -106,23 +107,31 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|     public MediaSourceManager(@NonNull final PlaybackListener listener, |     public MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                               @NonNull final PlayQueue playQueue) { |                               @NonNull final PlayQueue playQueue) { | ||||||
|         this(listener, playQueue, |         this(listener, playQueue, /*loadDebounceMillis=*/400L, | ||||||
|                 /*loadDebounceMillis=*/400L, |                 /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS), | ||||||
|                 /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); |                 /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private MediaSourceManager(@NonNull final PlaybackListener listener, |     private MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                                @NonNull final PlayQueue playQueue, |                                @NonNull final PlayQueue playQueue, | ||||||
|                                final long loadDebounceMillis, |                                final long loadDebounceMillis, | ||||||
|                                final long windowRefreshTimeMillis) { |                                final long playbackNearEndGapMillis, | ||||||
|  |                                final long progressUpdateIntervalMillis) { | ||||||
|         if (playQueue.getBroadcastReceiver() == null) { |         if (playQueue.getBroadcastReceiver() == null) { | ||||||
|             throw new IllegalArgumentException("Play Queue has not been initialized."); |             throw new IllegalArgumentException("Play Queue has not been initialized."); | ||||||
|         } |         } | ||||||
|  |         if (playbackNearEndGapMillis < progressUpdateIntervalMillis) { | ||||||
|  |             throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis + | ||||||
|  |                     " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis + | ||||||
|  |                     " ms] for them to be useful."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.playbackListener = listener; |         this.playbackListener = listener; | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|  |  | ||||||
|         this.windowRefreshTimeMillis = windowRefreshTimeMillis; |         this.playbackNearEndGapMillis = playbackNearEndGapMillis; | ||||||
|  |         this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; | ||||||
|  |         this.nearEndIntervalSignal = getEdgeIntervalSignal(); | ||||||
|  |  | ||||||
|         this.loadDebounceMillis = loadDebounceMillis; |         this.loadDebounceMillis = loadDebounceMillis; | ||||||
|         this.debouncedSignal = PublishSubject.create(); |         this.debouncedSignal = PublishSubject.create(); | ||||||
| @@ -161,28 +170,6 @@ public class MediaSourceManager { | |||||||
|         sources.releaseSource(); |         sources.releaseSource(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Loads the current playing stream and the streams within its windowSize bound. |  | ||||||
|      * |  | ||||||
|      * Unblocks the player once the item at the current index is loaded. |  | ||||||
|      * */ |  | ||||||
|     public void load() { |  | ||||||
|         if (DEBUG) Log.d(TAG, "load() called."); |  | ||||||
|         loadDebounced(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Blocks the player and repopulate the sources. |  | ||||||
|      * |  | ||||||
|      * Does not ensure the player is unblocked and should be done explicitly |  | ||||||
|      * through {@link #load() load}. |  | ||||||
|      * */ |  | ||||||
|     public void reset() { |  | ||||||
|         if (DEBUG) Log.d(TAG, "reset() called."); |  | ||||||
|  |  | ||||||
|         maybeBlock(); |  | ||||||
|         populateSources(); |  | ||||||
|     } |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Event Reactor |     // Event Reactor | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -219,11 +206,13 @@ public class MediaSourceManager { | |||||||
|         switch (event.type()) { |         switch (event.type()) { | ||||||
|             case INIT: |             case INIT: | ||||||
|             case ERROR: |             case ERROR: | ||||||
|                 reset(); |                 maybeBlock(); | ||||||
|                 break; |  | ||||||
|             case APPEND: |             case APPEND: | ||||||
|                 populateSources(); |                 populateSources(); | ||||||
|                 break; |                 break; | ||||||
|  |             case SELECT: | ||||||
|  |                 maybeRenewCurrentIndex(); | ||||||
|  |                 break; | ||||||
|             case REMOVE: |             case REMOVE: | ||||||
|                 final RemoveEvent removeEvent = (RemoveEvent) event; |                 final RemoveEvent removeEvent = (RemoveEvent) event; | ||||||
|                 remove(removeEvent.getRemoveIndex()); |                 remove(removeEvent.getRemoveIndex()); | ||||||
| @@ -238,7 +227,6 @@ public class MediaSourceManager { | |||||||
|                 final ReorderEvent reorderEvent = (ReorderEvent) event; |                 final ReorderEvent reorderEvent = (ReorderEvent) event; | ||||||
|                 move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); |                 move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); | ||||||
|                 break; |                 break; | ||||||
|             case SELECT: |  | ||||||
|             case RECOVERY: |             case RECOVERY: | ||||||
|             default: |             default: | ||||||
|                 break; |                 break; | ||||||
| @@ -280,15 +268,10 @@ public class MediaSourceManager { | |||||||
|     private boolean isPlaybackReady() { |     private boolean isPlaybackReady() { | ||||||
|         if (sources.getSize() != playQueue.size()) return false; |         if (sources.getSize() != playQueue.size()) return false; | ||||||
|  |  | ||||||
|         final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); |         final ManagedMediaSource mediaSource = | ||||||
|  |                 (ManagedMediaSource) sources.getMediaSource(playQueue.getIndex()); | ||||||
|         final PlayQueueItem playQueueItem = playQueue.getItem(); |         final PlayQueueItem playQueueItem = playQueue.getItem(); | ||||||
|  |         return mediaSource.isStreamEqual(playQueueItem); | ||||||
|         if (mediaSource instanceof LoadedMediaSource) { |  | ||||||
|             return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); |  | ||||||
|         } else if (mediaSource instanceof FailedMediaSource) { |  | ||||||
|             return playQueueItem == ((FailedMediaSource) mediaSource).getStream(); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void maybeBlock() { |     private void maybeBlock() { | ||||||
| @@ -319,7 +302,7 @@ public class MediaSourceManager { | |||||||
|         if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); |         if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); | ||||||
|  |  | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(); |         final PlayQueueItem currentItem = playQueue.getItem(); | ||||||
|         if (isBlocked.get() || currentItem == null) return; |         if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return; | ||||||
|  |  | ||||||
|         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); |         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); | ||||||
|         final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); |         final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); | ||||||
| @@ -347,8 +330,13 @@ public class MediaSourceManager { | |||||||
|     // MediaSource Loading |     // MediaSource Loading | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private Observable<Long> getEdgeIntervalSignal() { | ||||||
|  |         return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) | ||||||
|  |                 .filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private Disposable getDebouncedLoader() { |     private Disposable getDebouncedLoader() { | ||||||
|         return debouncedSignal |         return debouncedSignal.mergeWith(nearEndIntervalSignal) | ||||||
|                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) |                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(timestamp -> loadImmediate()); |                 .subscribe(timestamp -> loadImmediate()); | ||||||
| @@ -359,13 +347,14 @@ public class MediaSourceManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void loadImmediate() { |     private void loadImmediate() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called"); | ||||||
|         // The current item has higher priority |         // The current item has higher priority | ||||||
|         final int currentIndex = playQueue.getIndex(); |         final int currentIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); |         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); | ||||||
|         if (currentItem == null) return; |         if (currentItem == null) return; | ||||||
|  |  | ||||||
|         // Evict the items being loaded to free up memory |         // Evict the items being loaded to free up memory | ||||||
|         if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { |         if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) { | ||||||
|             loaderReactor.clear(); |             loaderReactor.clear(); | ||||||
|             loadingItems.clear(); |             loadingItems.clear(); | ||||||
|         } |         } | ||||||
| @@ -377,7 +366,7 @@ public class MediaSourceManager { | |||||||
|         final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); |         final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); | ||||||
|         final int rightLimit = currentIndex + WINDOW_SIZE + 1; |         final int rightLimit = currentIndex + WINDOW_SIZE + 1; | ||||||
|         final int rightBound = Math.min(playQueue.size(), rightLimit); |         final int rightBound = Math.min(playQueue.size(), rightLimit); | ||||||
|         final List<PlayQueueItem> items = new ArrayList<>( |         final Set<PlayQueueItem> items = new HashSet<>( | ||||||
|                 playQueue.getStreams().subList(leftBound,rightBound)); |                 playQueue.getStreams().subList(leftBound,rightBound)); | ||||||
|  |  | ||||||
|         // Do a round robin |         // Do a round robin | ||||||
| @@ -385,6 +374,7 @@ public class MediaSourceManager { | |||||||
|         if (excess >= 0) { |         if (excess >= 0) { | ||||||
|             items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); |             items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); | ||||||
|         } |         } | ||||||
|  |         items.remove(currentItem); | ||||||
|  |  | ||||||
|         for (final PlayQueueItem item : items) { |         for (final PlayQueueItem item : items) { | ||||||
|             maybeLoadItem(item); |             maybeLoadItem(item); | ||||||
| @@ -406,8 +396,6 @@ public class MediaSourceManager { | |||||||
|                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); |                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); | ||||||
|             loaderReactor.add(loader); |             loaderReactor.add(loader); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         maybeSynchronizePlayer(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { |     private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { | ||||||
| @@ -423,7 +411,8 @@ public class MediaSourceManager { | |||||||
|                 return new FailedMediaSource(stream, exception); |                 return new FailedMediaSource(stream, exception); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; |             final long expiration = System.currentTimeMillis() + | ||||||
|  |                     ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId()); | ||||||
|             return new LoadedMediaSource(source, stream, expiration); |             return new LoadedMediaSource(source, stream, expiration); | ||||||
|         }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); |         }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); | ||||||
|     } |     } | ||||||
| @@ -459,14 +448,37 @@ public class MediaSourceManager { | |||||||
|         if (index == -1 || index >= sources.getSize()) return false; |         if (index == -1 || index >= sources.getSize()) return false; | ||||||
|  |  | ||||||
|         final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); |         final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); | ||||||
|  |         return mediaSource.shouldBeReplacedWith(item, | ||||||
|         if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { |                 /*mightBeInProgress=*/index != playQueue.getIndex()); | ||||||
|             return item != ((LoadedMediaSource) mediaSource).getStream(); |  | ||||||
|         } else { |  | ||||||
|             return mediaSource.canReplace(item); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Checks if the current playing index contains an expired {@link ManagedMediaSource}. | ||||||
|  |      * If so, the expired source is replaced by a {@link PlaceholderMediaSource} and | ||||||
|  |      * {@link #loadImmediate()} is called to reload the current item. | ||||||
|  |      * <br><br> | ||||||
|  |      * If not, then the media source at the current index is ready for playback, and | ||||||
|  |      * {@link #maybeSynchronizePlayer()} is called. | ||||||
|  |      * <br><br> | ||||||
|  |      * Under both cases, {@link #maybeSync()} will be called to ensure the listener | ||||||
|  |      * is up-to-date. | ||||||
|  |      * */ | ||||||
|  |     private void maybeRenewCurrentIndex() { | ||||||
|  |         final int currentIndex = playQueue.getIndex(); | ||||||
|  |         if (sources.getSize() <= currentIndex) return; | ||||||
|  |  | ||||||
|  |         final ManagedMediaSource currentSource = | ||||||
|  |                 (ManagedMediaSource) sources.getMediaSource(currentIndex); | ||||||
|  |         final PlayQueueItem currentItem = playQueue.getItem(); | ||||||
|  |         if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) { | ||||||
|  |             maybeSynchronizePlayer(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + | ||||||
|  |                 "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); | ||||||
|  |         update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate); | ||||||
|  |     } | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // MediaSource Playlist Helpers |     // MediaSource Playlist Helpers | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -476,6 +488,7 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|         this.sources.releaseSource(); |         this.sources.releaseSource(); | ||||||
|         this.sources = new DynamicConcatenatingMediaSource(false, |         this.sources = new DynamicConcatenatingMediaSource(false, | ||||||
|  |                 // Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order | ||||||
|                 new ShuffleOrder.UnshuffledShuffleOrder(0)); |                 new ShuffleOrder.UnshuffledShuffleOrder(0)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem; | |||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| public interface PlaybackListener { | public interface PlaybackListener { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Called to check if the currently playing stream is close to the end of its playback. | ||||||
|  |      * Implementation should return true when the current playback position is within | ||||||
|  |      * timeToEndMillis or less until its playback completes or transitions. | ||||||
|  |      * | ||||||
|  |      * May be called at any time. | ||||||
|  |      * */ | ||||||
|  |     boolean isNearPlaybackEdge(final long timeToEndMillis); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when the stream at the current queue index is not ready yet. |      * Called when the stream at the current queue index is not ready yet. | ||||||
|      * Signals to the listener to block the player from playing anything and notify the source |      * Signals to the listener to block the player from playing anything and notify the source | ||||||
|   | |||||||
| @@ -11,20 +11,19 @@ import org.schabi.newpipe.util.ExtractorHelper; | |||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
|  |  | ||||||
| import io.reactivex.Single; | import io.reactivex.Single; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.functions.Consumer; |  | ||||||
| import io.reactivex.schedulers.Schedulers; | import io.reactivex.schedulers.Schedulers; | ||||||
|  |  | ||||||
| public class PlayQueueItem implements Serializable { | public class PlayQueueItem implements Serializable { | ||||||
|     final public static long RECOVERY_UNSET = Long.MIN_VALUE; |     public final static long RECOVERY_UNSET = Long.MIN_VALUE; | ||||||
|  |     private final static String EMPTY_STRING = ""; | ||||||
|  |  | ||||||
|     final private String title; |     @NonNull final private String title; | ||||||
|     final private String url; |     @NonNull final private String url; | ||||||
|     final private int serviceId; |     final private int serviceId; | ||||||
|     final private long duration; |     final private long duration; | ||||||
|     final private String thumbnailUrl; |     @NonNull final private String thumbnailUrl; | ||||||
|     final private String uploader; |     @NonNull final private String uploader; | ||||||
|     final private StreamType streamType; |     @NonNull final private StreamType streamType; | ||||||
|  |  | ||||||
|     private long recoveryPosition; |     private long recoveryPosition; | ||||||
|     private Throwable error; |     private Throwable error; | ||||||
| @@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable { | |||||||
|                 item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); |                 item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private PlayQueueItem(final String name, final String url, final int serviceId, |     private PlayQueueItem(@Nullable final String name, @Nullable final String url, | ||||||
|                           final long duration, final String thumbnailUrl, final String uploader, |                           final int serviceId, final long duration, | ||||||
|                           final StreamType streamType) { |                           @Nullable final String thumbnailUrl, @Nullable final String uploader, | ||||||
|         this.title = name; |                           @NonNull final StreamType streamType) { | ||||||
|         this.url = url; |         this.title = name != null ? name : EMPTY_STRING; | ||||||
|  |         this.url = url != null ? url : EMPTY_STRING; | ||||||
|         this.serviceId = serviceId; |         this.serviceId = serviceId; | ||||||
|         this.duration = duration; |         this.duration = duration; | ||||||
|         this.thumbnailUrl = thumbnailUrl; |         this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING; | ||||||
|         this.uploader = uploader; |         this.uploader = uploader != null ? uploader : EMPTY_STRING; | ||||||
|         this.streamType = streamType; |         this.streamType = streamType; | ||||||
|  |  | ||||||
|         this.recoveryPosition = RECOVERY_UNSET; |         this.recoveryPosition = RECOVERY_UNSET; | ||||||
| @@ -84,6 +84,7 @@ public class PlayQueueItem implements Serializable { | |||||||
|         return uploader; |         return uploader; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|     public StreamType getStreamType() { |     public StreamType getStreamType() { | ||||||
|         return streamType; |         return streamType; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,28 +1,22 @@ | |||||||
| package org.schabi.newpipe.playlist; | package org.schabi.newpipe.playlist; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.view.MotionEvent; | import android.view.MotionEvent; | ||||||
| import android.view.View; | import android.view.View; | ||||||
|  |  | ||||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| import com.nostra13.universalimageloader.core.assist.ImageScaleType; |  | ||||||
| import com.nostra13.universalimageloader.core.process.BitmapProcessor; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
|  |  | ||||||
|  |  | ||||||
| public class PlayQueueItemBuilder { | public class PlayQueueItemBuilder { | ||||||
|  |  | ||||||
|     private static final String TAG = PlayQueueItemBuilder.class.toString(); |     private static final String TAG = PlayQueueItemBuilder.class.toString(); | ||||||
|  |  | ||||||
|     private final int thumbnailWidthPx; |  | ||||||
|     private final int thumbnailHeightPx; |  | ||||||
|     private final DisplayImageOptions imageOptions; |  | ||||||
|  |  | ||||||
|     public interface OnSelectedListener { |     public interface OnSelectedListener { | ||||||
|         void selected(PlayQueueItem item, View view); |         void selected(PlayQueueItem item, View view); | ||||||
|         void held(PlayQueueItem item, View view); |         void held(PlayQueueItem item, View view); | ||||||
| @@ -31,11 +25,7 @@ public class PlayQueueItemBuilder { | |||||||
|  |  | ||||||
|     private OnSelectedListener onItemClickListener; |     private OnSelectedListener onItemClickListener; | ||||||
|  |  | ||||||
|     public PlayQueueItemBuilder(final Context context) { |     public PlayQueueItemBuilder(final Context context) {} | ||||||
|         thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width); |  | ||||||
|         thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height); |  | ||||||
|         imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setOnSelectedListener(OnSelectedListener listener) { |     public void setOnSelectedListener(OnSelectedListener listener) { | ||||||
|         this.onItemClickListener = listener; |         this.onItemClickListener = listener; | ||||||
| @@ -43,7 +33,8 @@ public class PlayQueueItemBuilder { | |||||||
|  |  | ||||||
|     public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { |     public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { | ||||||
|         if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); |         if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); | ||||||
|         if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader()); |         holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), | ||||||
|  |                 NewPipe.getNameOfService(item.getServiceId()))); | ||||||
|  |  | ||||||
|         if (item.getDuration() > 0) { |         if (item.getDuration() > 0) { | ||||||
|             holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); |             holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); | ||||||
| @@ -51,7 +42,8 @@ public class PlayQueueItemBuilder { | |||||||
|             holder.itemDurationView.setVisibility(View.GONE); |             holder.itemDurationView.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); |         ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, | ||||||
|  |                 ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         holder.itemRoot.setOnClickListener(view -> { |         holder.itemRoot.setOnClickListener(view -> { | ||||||
|             if (onItemClickListener != null) { |             if (onItemClickListener != null) { | ||||||
| @@ -81,23 +73,4 @@ public class PlayQueueItemBuilder { | |||||||
|             return false; |             return false; | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { |  | ||||||
|         final BitmapProcessor bitmapProcessor = bitmap -> { |  | ||||||
|             final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); |  | ||||||
|             bitmap.recycle(); |  | ||||||
|             return resizedBitmap; |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         return new DisplayImageOptions.Builder() |  | ||||||
|                 .showImageOnFail(R.drawable.dummy_thumbnail) |  | ||||||
|                 .showImageForEmptyUri(R.drawable.dummy_thumbnail) |  | ||||||
|                 .showImageOnLoading(R.drawable.dummy_thumbnail) |  | ||||||
|                 .bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways |  | ||||||
|                 .preProcessor(bitmapProcessor) |  | ||||||
|                 .imageScaleType(ImageScaleType.EXACTLY) |  | ||||||
|                 .cacheInMemory(true) |  | ||||||
|                 .cacheOnDisk(true) |  | ||||||
|                 .build(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,52 @@ | |||||||
|  | package org.schabi.newpipe.playlist; | ||||||
|  |  | ||||||
|  | import android.support.v7.widget.RecyclerView; | ||||||
|  | import android.support.v7.widget.helper.ItemTouchHelper; | ||||||
|  |  | ||||||
|  | public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback { | ||||||
|  |     private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10; | ||||||
|  |     private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25; | ||||||
|  |  | ||||||
|  |     public PlayQueueItemTouchCallback() { | ||||||
|  |         super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public abstract void onMove(final int sourceIndex, final int targetIndex); | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, | ||||||
|  |                                             int viewSizeOutOfBounds, int totalSize, | ||||||
|  |                                             long msSinceStartScroll) { | ||||||
|  |         final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, | ||||||
|  |                 viewSizeOutOfBounds, totalSize, msSinceStartScroll); | ||||||
|  |         final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY, | ||||||
|  |                 Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)); | ||||||
|  |         return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, | ||||||
|  |                           RecyclerView.ViewHolder target) { | ||||||
|  |         if (source.getItemViewType() != target.getItemViewType()) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final int sourceIndex = source.getLayoutPosition(); | ||||||
|  |         final int targetIndex = target.getLayoutPosition(); | ||||||
|  |         onMove(sourceIndex, targetIndex); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isLongPressDragEnabled() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isItemViewSwipeEnabled() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} | ||||||
|  | } | ||||||
| @@ -6,12 +6,14 @@ import android.content.DialogInterface; | |||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v7.preference.ListPreference; | import android.support.v7.preference.ListPreference; | ||||||
| import android.support.v7.preference.Preference; | import android.support.v7.preference.Preference; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
|  |  | ||||||
| import com.nononsenseapps.filepicker.Utils; | import com.nononsenseapps.filepicker.Utils; | ||||||
|  | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| @@ -47,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | |||||||
|     private File newpipe_db; |     private File newpipe_db; | ||||||
|     private File newpipe_db_journal; |     private File newpipe_db_journal; | ||||||
|  |  | ||||||
|  |     private String thumbnailLoadToggleKey; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onPreferenceTreeClick(Preference preference) { | ||||||
|  |         if (preference.getKey().equals(thumbnailLoadToggleKey)) { | ||||||
|  |             final ImageLoader imageLoader = ImageLoader.getInstance(); | ||||||
|  |             imageLoader.stop(); | ||||||
|  |             imageLoader.clearDiskCache(); | ||||||
|  |             imageLoader.clearMemoryCache(); | ||||||
|  |             imageLoader.resume(); | ||||||
|  |             Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice, | ||||||
|  |                     Toast.LENGTH_SHORT).show(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return super.onPreferenceTreeClick(preference); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,35 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v7.preference.Preference; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.util.InfoCache; | ||||||
|  |  | ||||||
| public class HistorySettingsFragment extends BasePreferenceFragment { | public class HistorySettingsFragment extends BasePreferenceFragment { | ||||||
|  |     private String cacheWipeKey; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         cacheWipeKey = getString(R.string.metadata_cache_wipe_key); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||||
|         addPreferencesFromResource(R.xml.history_settings); |         addPreferencesFromResource(R.xml.history_settings); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onPreferenceTreeClick(Preference preference) { | ||||||
|  |         if (preference.getKey().equals(cacheWipeKey)) { | ||||||
|  |             InfoCache.getInstance().clearCache(); | ||||||
|  |             Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, | ||||||
|  |                     Toast.LENGTH_SHORT).show(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return super.onPreferenceTreeClick(preference); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  |  | ||||||
|  | import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||||
|  | import com.nostra13.universalimageloader.core.assist.ImageScaleType; | ||||||
|  | import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  |  | ||||||
|  | public class ImageDisplayConstants { | ||||||
|  |     private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Base display options | ||||||
|  |      */ | ||||||
|  |     private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = | ||||||
|  |             new DisplayImageOptions.Builder() | ||||||
|  |                     .cacheInMemory(true) | ||||||
|  |                     .cacheOnDisk(true) | ||||||
|  |                     .resetViewBeforeLoading(true) | ||||||
|  |                     .bitmapConfig(Bitmap.Config.RGB_565) | ||||||
|  |                     .imageScaleType(ImageScaleType.EXACTLY) | ||||||
|  |                     .displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS)) | ||||||
|  |                     .build(); | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // DisplayImageOptions default configurations | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS = | ||||||
|  |             new DisplayImageOptions.Builder() | ||||||
|  |                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||||
|  |                     .showImageForEmptyUri(R.drawable.buddy) | ||||||
|  |                     .showImageOnFail(R.drawable.buddy) | ||||||
|  |                     .build(); | ||||||
|  |  | ||||||
|  |     public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = | ||||||
|  |             new DisplayImageOptions.Builder() | ||||||
|  |                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||||
|  |                     .showImageForEmptyUri(R.drawable.dummy_thumbnail) | ||||||
|  |                     .showImageOnFail(R.drawable.dummy_thumbnail) | ||||||
|  |                     .build(); | ||||||
|  |  | ||||||
|  |     public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS = | ||||||
|  |             new DisplayImageOptions.Builder() | ||||||
|  |                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||||
|  |                     .showImageForEmptyUri(R.drawable.channel_banner) | ||||||
|  |                     .showImageOnFail(R.drawable.channel_banner) | ||||||
|  |                     .build(); | ||||||
|  |  | ||||||
|  |     public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS = | ||||||
|  |             new DisplayImageOptions.Builder() | ||||||
|  |                     .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) | ||||||
|  |                     .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) | ||||||
|  |                     .showImageOnFail(R.drawable.dummy_thumbnail_playlist) | ||||||
|  |                     .build(); | ||||||
|  | } | ||||||
| @@ -43,7 +43,6 @@ public final class InfoCache { | |||||||
|      * Trim the cache to this size |      * Trim the cache to this size | ||||||
|      */ |      */ | ||||||
|     private static final int TRIM_CACHE_TO = 30; |     private static final int TRIM_CACHE_TO = 30; | ||||||
|     private static final int DEFAULT_TIMEOUT_HOURS = 4; |  | ||||||
|  |  | ||||||
|     private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); |     private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); | ||||||
|  |  | ||||||
| @@ -66,13 +65,7 @@ public final class InfoCache { | |||||||
|     public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { |     public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { | ||||||
|         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); |         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); | ||||||
|  |  | ||||||
|         final long expirationMillis; |         final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); | ||||||
|         if (info.getServiceId() == SoundCloud.getServiceId()) { |  | ||||||
|             expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); |  | ||||||
|         } else { |  | ||||||
|             expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         synchronized (lruCache) { |         synchronized (lruCache) { | ||||||
|             final CacheData data = new CacheData(info, expirationMillis); |             final CacheData data = new CacheData(info, expirationMillis); | ||||||
|             lruCache.put(keyOf(serviceId, url), data); |             lruCache.put(keyOf(serviceId, url), data); | ||||||
|   | |||||||
| @@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList; | |||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  |  | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; | ||||||
|  |  | ||||||
| public class ServiceHelper { | public class ServiceHelper { | ||||||
|     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; |     private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; | ||||||
|  |  | ||||||
| @@ -98,4 +102,12 @@ public class ServiceHelper { | |||||||
|         PreferenceManager.getDefaultSharedPreferences(context).edit(). |         PreferenceManager.getDefaultSharedPreferences(context).edit(). | ||||||
|                 putString(context.getString(R.string.current_service_key), serviceName).apply(); |                 putString(context.getString(R.string.current_service_key), serviceName).apply(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static long getCacheExpirationMillis(final int serviceId) { | ||||||
|  |         if (serviceId == SoundCloud.getServiceId()) { | ||||||
|  |             return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); | ||||||
|  |         } else { | ||||||
|  |             return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
|  | public interface SliderStrategy { | ||||||
|  |     /** | ||||||
|  |      * Converts from zeroed double with a minimum offset to the nearest rounded slider | ||||||
|  |      * equivalent integer | ||||||
|  |      * */ | ||||||
|  |     int progressOf(final double value); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts from slider integer value to an equivalent double value with a given | ||||||
|  |      * minimum offset | ||||||
|  |      * */ | ||||||
|  |     double valueOf(final int progress); | ||||||
|  |  | ||||||
|  |     // TODO: also implement linear strategy when needed | ||||||
|  |  | ||||||
|  |     final class Quadratic implements SliderStrategy { | ||||||
|  |         private final double leftGap; | ||||||
|  |         private final double rightGap; | ||||||
|  |         private final double center; | ||||||
|  |  | ||||||
|  |         private final int centerProgress; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Quadratic slider strategy that scales the value of a slider given how far the slider | ||||||
|  |          * progress is from the center of the slider. The further away from the center, | ||||||
|  |          * the faster the interpreted value changes, and vice versa. | ||||||
|  |          * | ||||||
|  |          * @param minimum the minimum value of the interpreted value of the slider. | ||||||
|  |          * @param maximum the maximum value of the interpreted value of the slider. | ||||||
|  |          * @param center center of the interpreted value between the minimum and maximum, which | ||||||
|  |          *               will be used as the center value on the slider progress. Doesn't need | ||||||
|  |          *               to be the average of the minimum and maximum values, but must be in | ||||||
|  |          *               between the two. | ||||||
|  |          * @param maxProgress the maximum possible progress of the slider, this is the | ||||||
|  |          *                    value that is shown for the UI and controls the granularity of | ||||||
|  |          *                    the slider. Should be as large as possible to avoid floating | ||||||
|  |          *                    point round-off error. Using odd number is recommended. | ||||||
|  |          * */ | ||||||
|  |         public Quadratic(double minimum, double maximum, double center, int maxProgress) { | ||||||
|  |             if (center < minimum || center > maximum) { | ||||||
|  |                 throw new IllegalArgumentException("Center must be in between minimum and maximum"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             this.leftGap = minimum - center; | ||||||
|  |             this.rightGap = maximum - center; | ||||||
|  |             this.center = center; | ||||||
|  |  | ||||||
|  |             this.centerProgress = maxProgress / 2; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public int progressOf(double value) { | ||||||
|  |             final double difference = value - center; | ||||||
|  |             final double root = difference >= 0 ? | ||||||
|  |                     Math.sqrt(difference / rightGap) : | ||||||
|  |                     -Math.sqrt(Math.abs(difference / leftGap)); | ||||||
|  |             final double offset = Math.round(root * centerProgress); | ||||||
|  |  | ||||||
|  |             return (int) (centerProgress + offset); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public double valueOf(int progress) { | ||||||
|  |             final int offset = progress - centerProgress; | ||||||
|  |             final double square = Math.pow(((double) offset) / ((double) centerProgress), 2); | ||||||
|  |             final double difference = square * (offset >= 0 ? rightGap : leftGap); | ||||||
|  |  | ||||||
|  |             return difference + center; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -301,9 +301,13 @@ | |||||||
|             android:id="@+id/live_sync" |             android:id="@+id/live_sync" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|  |             android:paddingLeft="4dp" | ||||||
|  |             android:paddingRight="4dp" | ||||||
|             android:gravity="center" |             android:gravity="center" | ||||||
|             android:text="@string/live_sync" |             android:text="@string/duration_live" | ||||||
|  |             android:textAllCaps="true" | ||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:maxLength="4" | ||||||
|             android:background="?attr/selectableItemBackground" |             android:background="?attr/selectableItemBackground" | ||||||
|             android:visibility="gone"/> |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ | |||||||
|         android:id="@+id/playQueuePanel" |         android:id="@+id/playQueuePanel" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent" |         android:layout_height="match_parent" | ||||||
|         android:visibility="gone" |         android:visibility="invisible" | ||||||
|         android:background="?attr/queue_background_color" |         android:background="?attr/queue_background_color" | ||||||
|         tools:visibility="visible"> |         tools:visibility="visible"> | ||||||
|  |  | ||||||
| @@ -254,7 +254,7 @@ | |||||||
|                     android:focusable="true" |                     android:focusable="true" | ||||||
|                     android:scaleType="fitXY" |                     android:scaleType="fitXY" | ||||||
|                     android:src="@drawable/ic_expand_more_white_24dp" |                     android:src="@drawable/ic_expand_more_white_24dp" | ||||||
|                     android:background="?attr/selectableItemBackground" |                     android:background="?attr/selectableItemBackgroundBorderless" | ||||||
|                     tools:ignore="ContentDescription,RtlHardcoded"/> |                     tools:ignore="ContentDescription,RtlHardcoded"/> | ||||||
|             </RelativeLayout> |             </RelativeLayout> | ||||||
|  |  | ||||||
| @@ -266,7 +266,7 @@ | |||||||
|                 android:gravity="top" |                 android:gravity="top" | ||||||
|                 android:paddingLeft="5dp" |                 android:paddingLeft="5dp" | ||||||
|                 android:paddingRight="5dp" |                 android:paddingRight="5dp" | ||||||
|                 android:visibility="gone" |                 android:visibility="invisible" | ||||||
|                 tools:ignore="RtlHardcoded" |                 tools:ignore="RtlHardcoded" | ||||||
|                 tools:visibility="visible"> |                 tools:visibility="visible"> | ||||||
|  |  | ||||||
| @@ -308,7 +308,7 @@ | |||||||
|                     android:id="@+id/toggleOrientation" |                     android:id="@+id/toggleOrientation" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="2dp" | ||||||
|                     android:layout_alignParentRight="true" |                     android:layout_alignParentRight="true" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
| @@ -325,8 +325,8 @@ | |||||||
|                     android:id="@+id/switchPopup" |                     android:id="@+id/switchPopup" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="4dp" | ||||||
|                     android:layout_toLeftOf="@id/toggleOrientation" |                     android:layout_toLeftOf="@id/toggleOrientation" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
|                     android:clickable="true" |                     android:clickable="true" | ||||||
| @@ -341,8 +341,8 @@ | |||||||
|                     android:id="@+id/switchBackground" |                     android:id="@+id/switchBackground" | ||||||
|                     android:layout_width="30dp" |                     android:layout_width="30dp" | ||||||
|                     android:layout_height="30dp" |                     android:layout_height="30dp" | ||||||
|                     android:layout_marginLeft="2dp" |                     android:layout_marginLeft="4dp" | ||||||
|                     android:layout_marginRight="2dp" |                     android:layout_marginRight="4dp" | ||||||
|                     android:layout_toLeftOf="@id/switchPopup" |                     android:layout_toLeftOf="@id/switchPopup" | ||||||
|                     android:layout_centerVertical="true" |                     android:layout_centerVertical="true" | ||||||
|                     android:clickable="true" |                     android:clickable="true" | ||||||
| @@ -403,9 +403,13 @@ | |||||||
|                     android:id="@+id/playbackLiveSync" |                     android:id="@+id/playbackLiveSync" | ||||||
|                     android:layout_width="wrap_content" |                     android:layout_width="wrap_content" | ||||||
|                     android:layout_height="match_parent" |                     android:layout_height="match_parent" | ||||||
|  |                     android:paddingLeft="4dp" | ||||||
|  |                     android:paddingRight="4dp" | ||||||
|                     android:gravity="center" |                     android:gravity="center" | ||||||
|                     android:text="@string/live_sync" |                     android:text="@string/duration_live" | ||||||
|  |                     android:textAllCaps="true" | ||||||
|                     android:textColor="@android:color/white" |                     android:textColor="@android:color/white" | ||||||
|  |                     android:maxLength="4" | ||||||
|                     android:visibility="gone" |                     android:visibility="gone" | ||||||
|                     android:background="?attr/selectableItemBackground" |                     android:background="?attr/selectableItemBackground" | ||||||
|                     tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> |                     tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|   | |||||||
| @@ -151,9 +151,13 @@ | |||||||
|             android:id="@+id/live_sync" |             android:id="@+id/live_sync" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|  |             android:paddingLeft="4dp" | ||||||
|  |             android:paddingRight="4dp" | ||||||
|             android:gravity="center" |             android:gravity="center" | ||||||
|             android:text="@string/live_sync" |             android:text="@string/duration_live" | ||||||
|  |             android:textAllCaps="true" | ||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:maxLength="4" | ||||||
|             android:background="?attr/selectableItemBackground" |             android:background="?attr/selectableItemBackground" | ||||||
|             android:visibility="gone"/> |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|   | |||||||
							
								
								
									
										313
									
								
								app/src/main/res/layout/dialog_playback_parameter.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								app/src/main/res/layout/dialog_playback_parameter.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <ScrollView 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:clickable="false" | ||||||
|  |     android:paddingLeft="@dimen/video_item_search_padding" | ||||||
|  |     android:paddingRight="@dimen/video_item_search_padding" | ||||||
|  |     android:paddingTop="@dimen/video_item_search_padding"> | ||||||
|  |  | ||||||
|  |     <RelativeLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:scrollbars="vertical" | ||||||
|  |         android:scrollbarAlwaysDrawVerticalTrack="true"> | ||||||
|  |  | ||||||
|  |         <!-- START HERE --> | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tempoControlText" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:layout_centerHorizontal="true" | ||||||
|  |             android:text="@string/playback_tempo" | ||||||
|  |             android:textStyle="bold" | ||||||
|  |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:layout_alignParentTop="true"/> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:id="@+id/tempoControl" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="40dp" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             android:layout_marginTop="4dp" | ||||||
|  |             android:layout_below="@id/tempoControlText"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tempoStepDown" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:clickable="true" | ||||||
|  |                 android:focusable="true" | ||||||
|  |                 android:text="--%" | ||||||
|  |                 android:textStyle="bold" | ||||||
|  |                 android:textColor="?attr/colorAccent" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:layout_alignParentLeft="true" | ||||||
|  |                 android:layout_alignParentStart="true" | ||||||
|  |                 tools:ignore="HardcodedText" | ||||||
|  |                 tools:text="-5%"/> | ||||||
|  |  | ||||||
|  |             <RelativeLayout | ||||||
|  |                 android:id="@+id/tempoDisplay" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:orientation="horizontal" | ||||||
|  |                 android:layout_marginLeft="4dp" | ||||||
|  |                 android:layout_marginRight="4dp" | ||||||
|  |                 android:layout_toRightOf="@id/tempoStepDown" | ||||||
|  |                 android:layout_toEndOf="@id/tempoStepDown" | ||||||
|  |                 android:layout_toLeftOf="@id/tempoStepUp" | ||||||
|  |                 android:layout_toStartOf="@id/tempoStepUp"> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tempoMinimumText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="-.--x" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_alignParentLeft="true" | ||||||
|  |                     android:layout_alignParentStart="true" | ||||||
|  |                     android:layout_marginLeft="4dp" | ||||||
|  |                     android:layout_marginStart="4dp" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="1.00x"/> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tempoCurrentText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="---%" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_centerHorizontal="true" | ||||||
|  |                     android:textStyle="bold" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="100%"/> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tempoMaximumText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="---%" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_alignParentRight="true" | ||||||
|  |                     android:layout_alignParentEnd="true" | ||||||
|  |                     android:layout_marginRight="4dp" | ||||||
|  |                     android:layout_marginEnd="4dp" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="300%"/> | ||||||
|  |  | ||||||
|  |                 <android.support.v7.widget.AppCompatSeekBar | ||||||
|  |                     android:id="@+id/tempoSeekbar" | ||||||
|  |                     style="@style/Widget.AppCompat.SeekBar" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_below="@id/tempoCurrentText" | ||||||
|  |                     android:paddingBottom="4dp" | ||||||
|  |                     tools:progress="50"/> | ||||||
|  |             </RelativeLayout> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tempoStepUp" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:clickable="true" | ||||||
|  |                 android:focusable="true" | ||||||
|  |                 android:text="+-%" | ||||||
|  |                 android:textStyle="bold" | ||||||
|  |                 android:textColor="?attr/colorAccent" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_alignParentRight="true" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_marginRight="4dp" | ||||||
|  |                 android:layout_marginEnd="4dp" | ||||||
|  |                 tools:ignore="HardcodedText" | ||||||
|  |                 tools:text="+5%"/> | ||||||
|  |         </RelativeLayout> | ||||||
|  |  | ||||||
|  |         <View | ||||||
|  |             android:id="@+id/separatorPitch" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="1dp" | ||||||
|  |             android:layout_below="@id/tempoControl" | ||||||
|  |             android:layout_margin="@dimen/video_item_search_padding" | ||||||
|  |             android:background="?attr/separator_color"/> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/pitchControlText" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:layout_centerHorizontal="true" | ||||||
|  |             android:text="@string/playback_pitch" | ||||||
|  |             android:textStyle="bold" | ||||||
|  |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:layout_below="@id/separatorPitch"/> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:id="@+id/pitchControl" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="40dp" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             android:layout_marginTop="4dp" | ||||||
|  |             android:layout_below="@id/pitchControlText"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/pitchStepDown" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:clickable="true" | ||||||
|  |                 android:focusable="true" | ||||||
|  |                 android:text="--%" | ||||||
|  |                 android:textStyle="bold" | ||||||
|  |                 android:textColor="?attr/colorAccent" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:layout_alignParentLeft="true" | ||||||
|  |                 android:layout_alignParentStart="true" | ||||||
|  |                 tools:ignore="HardcodedText" | ||||||
|  |                 tools:text="-5%"/> | ||||||
|  |  | ||||||
|  |             <RelativeLayout | ||||||
|  |                 android:id="@+id/pitchDisplay" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:orientation="horizontal" | ||||||
|  |                 android:layout_marginLeft="4dp" | ||||||
|  |                 android:layout_marginRight="4dp" | ||||||
|  |                 android:layout_toRightOf="@+id/pitchStepDown" | ||||||
|  |                 android:layout_toEndOf="@+id/pitchStepDown" | ||||||
|  |                 android:layout_toLeftOf="@+id/pitchStepUp" | ||||||
|  |                 android:layout_toStartOf="@+id/pitchStepUp"> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/pitchMinimumText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="---%" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_alignParentLeft="true" | ||||||
|  |                     android:layout_alignParentStart="true" | ||||||
|  |                     android:layout_marginLeft="4dp" | ||||||
|  |                     android:layout_marginStart="4dp" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="25%"/> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/pitchCurrentText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="---%" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_centerHorizontal="true" | ||||||
|  |                     android:textStyle="bold" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="100%"/> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/pitchMaximumText" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="---%" | ||||||
|  |                     android:textColor="?attr/colorAccent" | ||||||
|  |                     android:layout_alignParentRight="true" | ||||||
|  |                     android:layout_alignParentEnd="true" | ||||||
|  |                     android:layout_marginRight="4dp" | ||||||
|  |                     android:layout_marginEnd="4dp" | ||||||
|  |                     tools:ignore="HardcodedText" | ||||||
|  |                     tools:text="300%"/> | ||||||
|  |  | ||||||
|  |                 <android.support.v7.widget.AppCompatSeekBar | ||||||
|  |                     android:id="@+id/pitchSeekbar" | ||||||
|  |                     style="@style/Widget.AppCompat.SeekBar" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_below="@+id/pitchCurrentText" | ||||||
|  |                     android:paddingBottom="4dp" | ||||||
|  |                     tools:progress="50"/> | ||||||
|  |             </RelativeLayout> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/pitchStepUp" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:clickable="true" | ||||||
|  |                 android:focusable="true" | ||||||
|  |                 android:text="+-%" | ||||||
|  |                 android:textStyle="bold" | ||||||
|  |                 android:textColor="?attr/colorAccent" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_alignParentRight="true" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_marginRight="4dp" | ||||||
|  |                 android:layout_marginEnd="4dp" | ||||||
|  |                 tools:ignore="HardcodedText" | ||||||
|  |                 tools:text="+5%"/> | ||||||
|  |         </RelativeLayout> | ||||||
|  |  | ||||||
|  |         <View | ||||||
|  |             android:id="@+id/separatorCheckbox" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="1dp" | ||||||
|  |             android:layout_below="@+id/pitchControl" | ||||||
|  |             android:layout_margin="@dimen/video_item_search_padding" | ||||||
|  |             android:background="?attr/separator_color"/> | ||||||
|  |  | ||||||
|  |         <CheckBox | ||||||
|  |             android:id="@+id/unhookCheckbox" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:checked="false" | ||||||
|  |             android:clickable="true" | ||||||
|  |             android:focusable="true" | ||||||
|  |             android:text="@string/unhook_checkbox" | ||||||
|  |             android:maxLines="1" | ||||||
|  |             android:layout_centerHorizontal="true" | ||||||
|  |             android:layout_below="@id/separatorCheckbox"/> | ||||||
|  |  | ||||||
|  |         <LinearLayout | ||||||
|  |             android:id="@+id/presetSelector" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="40dp" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             android:layout_below="@id/unhookCheckbox"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/presetNightcore" | ||||||
|  |                 android:layout_width="0dp" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:layout_weight="1" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:text="@string/playback_nightcore" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:textColor="?attr/colorAccent"/> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/presetReset" | ||||||
|  |                 android:layout_width="0dp" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:layout_weight="1" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:text="@string/playback_default" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 android:textColor="?attr/colorAccent"/> | ||||||
|  |         </LinearLayout> | ||||||
|  |  | ||||||
|  |         <!-- END HERE --> | ||||||
|  |  | ||||||
|  |     </RelativeLayout> | ||||||
|  | </ScrollView> | ||||||
| @@ -19,7 +19,7 @@ | |||||||
|         android:layout_alignParentTop="true" |         android:layout_alignParentTop="true" | ||||||
|         android:layout_marginRight="@dimen/video_item_search_image_right_margin" |         android:layout_marginRight="@dimen/video_item_search_image_right_margin" | ||||||
|         android:contentDescription="@string/list_thumbnail_view_description" |         android:contentDescription="@string/list_thumbnail_view_description" | ||||||
|         android:scaleType="fitEnd" |         android:scaleType="centerCrop" | ||||||
|         android:src="@drawable/dummy_thumbnail_playlist" |         android:src="@drawable/dummy_thumbnail_playlist" | ||||||
|         tools:ignore="RtlHardcoded"/> |         tools:ignore="RtlHardcoded"/> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -195,9 +195,13 @@ | |||||||
|                 android:id="@+id/playbackLiveSync" |                 android:id="@+id/playbackLiveSync" | ||||||
|                 android:layout_width="wrap_content" |                 android:layout_width="wrap_content" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|  |                 android:paddingLeft="4dp" | ||||||
|  |                 android:paddingRight="4dp" | ||||||
|                 android:gravity="center_vertical" |                 android:gravity="center_vertical" | ||||||
|                 android:text="@string/live_sync" |                 android:text="@string/duration_live" | ||||||
|  |                 android:textAllCaps="true" | ||||||
|                 android:textColor="@android:color/white" |                 android:textColor="@android:color/white" | ||||||
|  |                 android:maxLength="4" | ||||||
|                 android:visibility="gone" |                 android:visibility="gone" | ||||||
|                 android:background="?attr/selectableItemBackground" |                 android:background="?attr/selectableItemBackground" | ||||||
|                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> |                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|   | |||||||
| @@ -160,6 +160,10 @@ | |||||||
|     <string name="import_data">import_data</string> |     <string name="import_data">import_data</string> | ||||||
|     <string name="export_data">export_data</string> |     <string name="export_data">export_data</string> | ||||||
|  |  | ||||||
|  |     <string name="download_thumbnail_key" translatable="false">download_thumbnail_key</string> | ||||||
|  |  | ||||||
|  |     <string name="metadata_cache_wipe_key" translatable="false">cache_wipe_key</string> | ||||||
|  |  | ||||||
|     <!-- FileName Downloads  --> |     <!-- FileName Downloads  --> | ||||||
|     <string name="settings_file_charset_key" translatable="false">file_rename</string> |     <string name="settings_file_charset_key" translatable="false">file_rename</string> | ||||||
|     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> |     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> | ||||||
|   | |||||||
| @@ -74,6 +74,12 @@ | |||||||
|     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> |     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> | ||||||
|     <string name="use_inexact_seek_title">Use fast inexact seek</string> |     <string name="use_inexact_seek_title">Use fast inexact seek</string> | ||||||
|     <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> |     <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> | ||||||
|  |     <string name="download_thumbnail_title">Load thumbnails</string> | ||||||
|  |     <string name="download_thumbnail_summary">Disable to stop all thumbnails from loading and save on data and memory usage. Changing this will clear both in-memory and on-disk image cache.</string> | ||||||
|  |     <string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string> | ||||||
|  |     <string name="metadata_cache_wipe_title">Wipe cached metadata</string> | ||||||
|  |     <string name="metadata_cache_wipe_summary">Remove all cached webpage data</string> | ||||||
|  |     <string name="metadata_cache_wipe_complete_notice">Metadata cache wiped</string> | ||||||
|     <string name="auto_queue_title">Auto-queue next stream</string> |     <string name="auto_queue_title">Auto-queue next stream</string> | ||||||
|     <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> |     <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> | ||||||
|     <string name="player_gesture_controls_title">Player gesture controls</string> |     <string name="player_gesture_controls_title">Player gesture controls</string> | ||||||
| @@ -89,7 +95,7 @@ | |||||||
|     <string name="download_dialog_title">Download</string> |     <string name="download_dialog_title">Download</string> | ||||||
|     <string name="next_video_title">Next video</string> |     <string name="next_video_title">Next video</string> | ||||||
|     <string name="show_next_and_similar_title">Show next and similar videos</string> |     <string name="show_next_and_similar_title">Show next and similar videos</string> | ||||||
|     <string name="show_hold_to_append_title">Show Hold to Append Tip</string> |     <string name="show_hold_to_append_title">Show hold to append tip</string> | ||||||
|     <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> |     <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> | ||||||
|     <string name="url_not_supported_toast">URL not supported</string> |     <string name="url_not_supported_toast">URL not supported</string> | ||||||
|     <string name="default_content_country_title">Default content country</string> |     <string name="default_content_country_title">Default content country</string> | ||||||
| @@ -98,7 +104,7 @@ | |||||||
|     <string name="settings_category_player_title">Player</string> |     <string name="settings_category_player_title">Player</string> | ||||||
|     <string name="settings_category_player_behavior_title">Behavior</string> |     <string name="settings_category_player_behavior_title">Behavior</string> | ||||||
|     <string name="settings_category_video_audio_title">Video & Audio</string> |     <string name="settings_category_video_audio_title">Video & Audio</string> | ||||||
|     <string name="settings_category_history_title">History</string> |     <string name="settings_category_history_title">History & Cache</string> | ||||||
|     <string name="settings_category_popup_title">Popup</string> |     <string name="settings_category_popup_title">Popup</string> | ||||||
|     <string name="settings_category_appearance_title">Appearance</string> |     <string name="settings_category_appearance_title">Appearance</string> | ||||||
|     <string name="settings_category_other_title">Other</string> |     <string name="settings_category_other_title">Other</string> | ||||||
| @@ -418,18 +424,16 @@ | |||||||
|     <string name="resize_zoom">ZOOM</string> |     <string name="resize_zoom">ZOOM</string> | ||||||
|  |  | ||||||
|     <string name="caption_auto_generated">Auto-generated</string> |     <string name="caption_auto_generated">Auto-generated</string> | ||||||
|     <string name="caption_font_size_settings_title">Caption Font Size</string> |     <string name="caption_font_size_settings_title">Caption font size</string> | ||||||
|     <string name="smaller_caption_font_size">Smaller Font</string> |     <string name="smaller_caption_font_size">Smaller font</string> | ||||||
|     <string name="normal_caption_font_size">Normal Font</string> |     <string name="normal_caption_font_size">Normal font</string> | ||||||
|     <string name="larger_caption_font_size">Larger Font</string> |     <string name="larger_caption_font_size">Larger font</string> | ||||||
|  |  | ||||||
|     <string name="live_sync">SYNC</string> |  | ||||||
|  |  | ||||||
|     <!-- Debug Settings --> |     <!-- Debug Settings --> | ||||||
|     <string name="enable_leak_canary_title">Enable LeakCanary</string> |     <string name="enable_leak_canary_title">Enable LeakCanary</string> | ||||||
|     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> |     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> | ||||||
|  |  | ||||||
|     <string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string> |     <string name="enable_disposed_exceptions_title">Report Out-of-lifecycle errors</string> | ||||||
|     <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> |     <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> | ||||||
|  |  | ||||||
|     <!-- Subscriptions import/export --> |     <!-- Subscriptions import/export --> | ||||||
| @@ -452,4 +456,12 @@ | |||||||
|     <string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string> |     <string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string> | ||||||
|  |  | ||||||
|     <string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string> |     <string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string> | ||||||
|  |  | ||||||
|  |     <!-- Playback Parameters --> | ||||||
|  |     <string name="playback_speed_control">Playback Speed Control</string> | ||||||
|  |     <string name="playback_tempo">Tempo</string> | ||||||
|  |     <string name="playback_pitch">Pitch</string> | ||||||
|  |     <string name="unhook_checkbox">Unhook (may cause distortion)</string> | ||||||
|  |     <string name="playback_nightcore">Nightcore</string> | ||||||
|  |     <string name="playback_default">Default</string> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -37,6 +37,12 @@ | |||||||
|         android:summary="@string/auto_queue_summary" |         android:summary="@string/auto_queue_summary" | ||||||
|         android:title="@string/auto_queue_title"/> |         android:title="@string/auto_queue_title"/> | ||||||
|  |  | ||||||
|  |     <SwitchPreference | ||||||
|  |         android:defaultValue="true" | ||||||
|  |         android:key="@string/download_thumbnail_key" | ||||||
|  |         android:title="@string/download_thumbnail_title" | ||||||
|  |         android:summary="@string/download_thumbnail_summary"/> | ||||||
|  |  | ||||||
|     <ListPreference |     <ListPreference | ||||||
|         android:defaultValue="@string/kiosk_page_key" |         android:defaultValue="@string/kiosk_page_key" | ||||||
|         android:entries="@array/main_page_content_names" |         android:entries="@array/main_page_content_names" | ||||||
|   | |||||||
| @@ -16,4 +16,9 @@ | |||||||
|         android:summary="@string/enable_search_history_summary" |         android:summary="@string/enable_search_history_summary" | ||||||
|         android:title="@string/enable_search_history_title"/> |         android:title="@string/enable_search_history_title"/> | ||||||
|  |  | ||||||
|  |     <Preference | ||||||
|  |         android:key="@string/metadata_cache_wipe_key" | ||||||
|  |         android:summary="@string/metadata_cache_wipe_summary" | ||||||
|  |         android:title="@string/metadata_cache_wipe_title"/> | ||||||
|  |  | ||||||
| </PreferenceScreen> | </PreferenceScreen> | ||||||
|   | |||||||
| @@ -0,0 +1,86 @@ | |||||||
|  | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
|  | import org.junit.Test; | ||||||
|  | import static org.junit.Assert.assertEquals; | ||||||
|  | import static org.junit.Assert.assertTrue; | ||||||
|  |  | ||||||
|  | public class QuadraticSliderStrategyTest { | ||||||
|  |     private final static int STEP = 100; | ||||||
|  |     private final static float DELTA = 1f / (float) STEP; | ||||||
|  |  | ||||||
|  |     private final SliderStrategy.Quadratic standard = | ||||||
|  |             new SliderStrategy.Quadratic(0f, 100f, 50f, STEP); | ||||||
|  |     @Test | ||||||
|  |     public void testLeftBound() throws Exception { | ||||||
|  |         assertEquals(standard.progressOf(0), 0); | ||||||
|  |         assertEquals(standard.valueOf(0), 0f, DELTA); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testCenter() throws Exception { | ||||||
|  |         assertEquals(standard.progressOf(50), 50); | ||||||
|  |         assertEquals(standard.valueOf(50), 50f, DELTA); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testRightBound() throws Exception { | ||||||
|  |         assertEquals(standard.progressOf(100), 100); | ||||||
|  |         assertEquals(standard.valueOf(100), 100f, DELTA); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testLeftRegion() throws Exception { | ||||||
|  |         final int leftProgress = standard.progressOf(25); | ||||||
|  |         final double leftValue = standard.valueOf(25); | ||||||
|  |         assertTrue(leftProgress > 0 && leftProgress < 50); | ||||||
|  |         assertTrue(leftValue > 0f && leftValue < 50); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testRightRegion() throws Exception { | ||||||
|  |         final int leftProgress = standard.progressOf(75); | ||||||
|  |         final double leftValue = standard.valueOf(75); | ||||||
|  |         assertTrue(leftProgress > 50 && leftProgress < 100); | ||||||
|  |         assertTrue(leftValue > 50f && leftValue < 100); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testConversion() throws Exception { | ||||||
|  |         assertEquals(standard.progressOf(standard.valueOf(0)), 0); | ||||||
|  |         assertEquals(standard.progressOf(standard.valueOf(25)), 25); | ||||||
|  |         assertEquals(standard.progressOf(standard.valueOf(50)), 50); | ||||||
|  |         assertEquals(standard.progressOf(standard.valueOf(75)), 75); | ||||||
|  |         assertEquals(standard.progressOf(standard.valueOf(100)), 100); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testReverseConversion() throws Exception { | ||||||
|  |         // Need a larger delta since step size / granularity is too small and causes | ||||||
|  |         // floating point round-off errors during conversion | ||||||
|  |         final float largeDelta = 1f; | ||||||
|  |  | ||||||
|  |         assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta); | ||||||
|  |         assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta); | ||||||
|  |         assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta); | ||||||
|  |         assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta); | ||||||
|  |         assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testQuadraticPropertyLeftRegion() throws Exception { | ||||||
|  |         final double differenceCloserToCenter = | ||||||
|  |                 Math.abs(standard.valueOf(40) - standard.valueOf(45)); | ||||||
|  |         final double differenceFurtherFromCenter = | ||||||
|  |                 Math.abs(standard.valueOf(10) - standard.valueOf(15)); | ||||||
|  |         assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testQuadraticPropertyRightRegion() throws Exception { | ||||||
|  |         final double differenceCloserToCenter = | ||||||
|  |                 Math.abs(standard.valueOf(75) - standard.valueOf(70)); | ||||||
|  |         final double differenceFurtherFromCenter = | ||||||
|  |                 Math.abs(standard.valueOf(95) - standard.valueOf(90)); | ||||||
|  |         assertTrue(differenceCloserToCenter < differenceFurtherFromCenter); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Christian Schabesberger
					Christian Schabesberger