Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Weblate 2017-10-31 20:46:09 +01:00
commit b5d7b80fe9
98 changed files with 6203 additions and 1531 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "app/src/main/java/org/schabi/newpipe/extractor"]
path = app/src/main/java/org/schabi/newpipe/extractor
url = https://github.com/TeamNewPipe/NewPipeExtractor.git

View File

@ -56,13 +56,13 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
* Subscribe to channels
* Search history
* Search/Watch Playlists
* Watch as queues Playlists
* Queuing videos
### Coming Features
* Multiservice support (eg. SoundCloud)
* Bookmarks
* Watch as queues Playlists
* Queuing videos
* Subtitles support
* livestream support
* ... and many more

View File

@ -48,7 +48,7 @@ dependencies {
exclude module: 'support-annotations'
}
compile 'com.github.TeamNewPipe:NewPipeExtractor:7899cd1'
compile 'com.github.TeamNewPipe:NewPipeExtractor:b9d0941'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
@ -66,7 +66,7 @@ dependencies {
compile 'de.hdodenhof:circleimageview:2.1.0'
compile 'com.github.nirhart:parallaxscroll:1.0'
compile 'com.nononsenseapps:filepicker:3.0.1'
compile 'com.google.android.exoplayer:exoplayer:r2.5.1'
compile 'com.google.android.exoplayer:exoplayer:r2.5.4'
debugCompile 'com.facebook.stetho:stetho:1.5.0'
debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0'

View File

@ -38,6 +38,16 @@
android:name=".player.BackgroundPlayer"
android:exported="false"/>
<activity
android:name=".player.BackgroundPlayerActivity"
android:launchMode="singleTask"
android:label="@string/title_activity_background_player"/>
<activity
android:name=".player.PopupVideoPlayerActivity"
android:launchMode="singleTask"
android:label="@string/title_activity_popup_player"/>
<service
android:name=".player.PopupVideoPlayer"
android:exported="false"/>

View File

@ -27,6 +27,7 @@ import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
@ -309,7 +310,7 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
}
@Override
public void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream) {
public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) {
addWatchHistoryEntry(streamInfo);
}

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.fragments.detail;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@ -28,6 +29,7 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@ -60,9 +62,12 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
@ -89,12 +94,11 @@ import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener {
public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, View.OnLongClickListener {
public static final String AUTO_PLAY = "auto_play";
// Amount of videos to show on start
private static final int INITIAL_RELATED_VIDEOS = 8;
private static final String KORE_PACKET = "org.xbmc.kore";
private ActionBarHandler actionBarHandler;
private ArrayList<VideoStream> sortedStreamVideosList;
@ -141,6 +145,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
private TextView detailControlsBackground;
private TextView detailControlsPopup;
private TextView appendControlsDetail;
private LinearLayout videoDescriptionRootLayout;
private TextView videoUploadDateView;
@ -317,10 +322,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
switch (v.getId()) {
case R.id.detail_controls_background:
openBackgroundPlayer();
openBackgroundPlayer(false);
break;
case R.id.detail_controls_popup:
openPopupPlayer();
openPopupPlayer(false);
break;
case R.id.detail_uploader_root_layout:
if (currentInfo.uploader_url == null || currentInfo.uploader_url.isEmpty()) {
@ -341,6 +346,22 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
}
}
@Override
public boolean onLongClick(View v) {
if (isLoading.get() || currentInfo == null) return false;
switch (v.getId()) {
case R.id.detail_controls_background:
openBackgroundPlayer(true);
break;
case R.id.detail_controls_popup:
openPopupPlayer(true);
break;
}
return true;
}
private void toggleTitleAndDescription() {
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
@ -400,6 +421,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
@ -445,6 +467,32 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
detailControlsBackground.setOnClickListener(this);
detailControlsPopup.setOnClickListener(this);
relatedStreamExpandButton.setOnClickListener(this);
detailControlsBackground.setLongClickable(true);
detailControlsPopup.setLongClickable(true);
detailControlsBackground.setOnLongClickListener(this);
detailControlsPopup.setOnLongClickListener(this);
detailControlsBackground.setOnTouchListener(getOnControlsTouchListener());
detailControlsPopup.setOnTouchListener(getOnControlsTouchListener());
}
private View.OnTouchListener getOnControlsTouchListener() {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false;
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
animateView(appendControlsDetail, true, 250, 0, new Runnable() {
@Override
public void run() {
animateView(appendControlsDetail, false, 1500, 1000);
}
});
}
return false;
}
};
}
private void initThumbnailViews(StreamInfo info) {
@ -513,6 +561,24 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
return (!isLoading.get() && actionBarHandler.onItemSelected(item)) || super.onOptionsItemSelected(item);
}
private static void showInstallKoreDialog(final Context context) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
NavigationHelper.installKore(context);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
private void setupActionBarHandler(final StreamInfo info) {
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(activity, info.video_streams, info.video_only_streams, false));
@ -542,30 +608,13 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
@Override
public void onActionSelected(int selectedStreamId) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(KORE_PACKET);
intent.setData(Uri.parse(info.url.replace("https", "http")));
activity.startActivity(intent);
NavigationHelper.playWithKore(activity, Uri.parse(info.url.replace("https", "http")));
if(activity instanceof HistoryListener) {
((HistoryListener) activity).onVideoPlayed(info, null);
}
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
showInstallKoreDialog(activity);
}
}
});
@ -713,7 +762,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
// Play Utils
//////////////////////////////////////////////////////////////////////////*/
private void openBackgroundPlayer() {
private void openBackgroundPlayer(final boolean append) {
AudioStream audioStream = currentInfo.audio_streams.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.audio_streams));
if (activity instanceof HistoryListener) {
@ -724,13 +773,13 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
openNormalBackgroundPlayer(audioStream);
openNormalBackgroundPlayer(append);
} else {
openExternalBackgroundPlayer(audioStream);
}
}
private void openPopupPlayer() {
private void openPopupPlayer(final boolean append) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) {
Toast toast = Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG);
TextView messageView = toast.getView().findViewById(android.R.id.message);
@ -743,9 +792,16 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream());
}
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, currentInfo, actionBarHandler.getSelectedVideoStream());
activity.startService(mIntent);
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
final Intent intent;
if (append) {
Toast.makeText(activity, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
intent = NavigationHelper.getPlayerEnqueueIntent(activity, PopupVideoPlayer.class, playQueue);
} else {
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
intent = NavigationHelper.getPlayerIntent(activity, PopupVideoPlayer.class, playQueue, getSelectedVideoStream().resolution);
}
activity.startService(intent);
}
private void openVideoPlayer() {
@ -763,9 +819,15 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
}
private void openNormalBackgroundPlayer(AudioStream audioStream) {
activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, currentInfo, audioStream));
Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
private void openNormalBackgroundPlayer(final boolean append) {
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
if (append) {
activity.startService(NavigationHelper.getPlayerEnqueueIntent(activity, BackgroundPlayer.class, playQueue));
Toast.makeText(activity, R.string.background_player_append, Toast.LENGTH_SHORT).show();
} else {
activity.startService(NavigationHelper.getPlayerIntent(activity, BackgroundPlayer.class, playQueue));
Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
}
}
private void openExternalBackgroundPlayer(AudioStream audioStream) {
@ -808,7 +870,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|| (Build.VERSION.SDK_INT < 16);
if (!useOldPlayer) {
// ExoPlayer
mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, MainVideoPlayer.class, currentInfo, actionBarHandler.getSelectedVideoStream());
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, getSelectedVideoStream().resolution);
} else {
// Internal Player
mIntent = new Intent(activity, PlayVideoActivity.class)

View File

@ -187,7 +187,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
supportActionBar.setDisplayHomeAsUpEnabled(true);
if(useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
}
}

View File

@ -1,17 +1,22 @@
package org.schabi.newpipe.fragments.list.playlist;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
@ -19,9 +24,15 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.playlist.ExternalPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import io.reactivex.Single;
@ -40,6 +51,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private ImageView headerUploaderAvatar;
private TextView headerStreamCount;
private Button headerPlayAllButton;
private Button headerPopupButton;
private Button headerBackgroundButton;
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
PlaylistFragment instance = new PlaylistFragment();
instance.setInitialData(serviceId, url, name);
@ -67,6 +82,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_play_bg_button);
return headerRootLayout;
}
@ -132,11 +151,48 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
imageLoader.displayImage(result.uploader_avatar_url, headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(result.stream_count + " videos");
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0);
}
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(buildPlaylistIntent(MainVideoPlayer.class));
}
});
headerPopupButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) {
Toast toast = Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG);
TextView messageView = toast.getView().findViewById(android.R.id.message);
if (messageView != null) messageView.setGravity(Gravity.CENTER);
toast.show();
return;
}
activity.startService(buildPlaylistIntent(PopupVideoPlayer.class));
}
});
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
activity.startService(buildPlaylistIntent(BackgroundPlayer.class));
}
});
}
private Intent buildPlaylistIntent(final Class targetClazz) {
final PlayQueue playQueue = new ExternalPlayQueue(
currentInfo.service_id,
currentInfo.url,
currentInfo.next_streams_url,
infoListAdapter.getItemsList(),
0
);
return NavigationHelper.getPlayerIntent(activity, targetClazz, playQueue);
}
@Override

View File

@ -113,6 +113,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
private int currentNextPage = 0;
private String searchLanguage;
private boolean isSuggestionsEnabled = true;
private boolean isSearchHistoryEnabled = true;
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
private Disposable searchDisposable;
@ -160,7 +161,12 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onAttach(Context context) {
super.onAttach(context);
suggestionListAdapter = new SuggestionListAdapter(activity);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSugestinHistory(isSearchHistoryEnabled);
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO();
}

View File

@ -19,6 +19,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context;
private OnSuggestionItemSelected listener;
private boolean showSugestinHistory = true;
public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item);
@ -31,7 +32,16 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
public void setItems(List<SuggestionItem> items) {
this.items.clear();
this.items.addAll(items);
if (showSugestinHistory) {
this.items.addAll(items);
} else {
// remove history items if history is disabled
for (SuggestionItem item : items) {
if (!item.fromHistory) {
this.items.add(item);
}
}
}
notifyDataSetChanged();
}
@ -39,6 +49,10 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
this.listener = listener;
}
public void setShowSugestinHistory(boolean v) {
showSugestinHistory = v;
}
@Override
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));

View File

@ -7,7 +7,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -21,6 +20,7 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;

View File

@ -55,7 +55,7 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
private RecyclerView mRecyclerView;
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
// private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
private HistoryDAO<E> mHistoryDataSource;
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
@ -99,7 +99,11 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
}
});
mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, allowedSwipeToDeleteDirections) {
}
protected void historyItemSwipeCallback(int swipeDirection) {
mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
@ -241,6 +245,7 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
if (mHistoryIsEnabled) {
mRecyclerView.setVisibility(View.VISIBLE);
} else {
mRecyclerView.setVisibility(View.GONE);
mDisabledView.setVisibility(View.VISIBLE);
}
@ -264,10 +269,6 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState();
}
public void setAllowedSwipeToDeleteDirections(int allowedSwipeToDeleteDirections) {
this.allowedSwipeToDeleteDirections = allowedSwipeToDeleteDirections;
}
/**
* Called when history enabled flag is changed.
*

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.history;
import android.support.annotation.Nullable;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
@ -9,9 +11,10 @@ public interface HistoryListener {
* Called when a video is played
*
* @param streamInfo the stream info
* @param videoStream the video stream that is played
* @param videoStream the video stream that is played. Can be null if it's not sure what
* quality was viewed (e.g. with Kodi).
*/
void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream);
void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream);
/**
* Called when the audio is played in the background

View File

@ -1,9 +1,12 @@
package org.schabi.newpipe.history;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -17,11 +20,19 @@ import org.schabi.newpipe.util.NavigationHelper;
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT;
@NonNull
public static SearchHistoryFragment newInstance() {
return new SearchHistoryFragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
}
@NonNull
@Override
protected SearchHistoryAdapter createAdapter() {

View File

@ -7,6 +7,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -26,6 +27,8 @@ import org.schabi.newpipe.util.NavigationHelper;
public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT;
@NonNull
public static WatchedHistoryFragment newInstance() {
return new WatchedHistoryFragment();
@ -34,7 +37,7 @@ public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
}
@StringRes

View File

@ -26,25 +26,31 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.RemoteViews;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
/**
@ -52,38 +58,39 @@ import java.io.Serializable;
*
* @author mauriciocolli
*/
public class BackgroundPlayer extends Service {
public final class BackgroundPlayer extends Service {
private static final String TAG = "BackgroundPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE";
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.BackgroundPlayer.OPEN_DETAIL";
public static final String ACTION_OPEN_CONTROLS = "org.schabi.newpipe.player.BackgroundPlayer.OPEN_CONTROLS";
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
public static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND";
public static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD";
public static final String AUDIO_STREAM = "video_only_audio_stream";
private AudioStream audioStream;
public static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT";
public static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS";
private BasePlayerImpl basePlayerImpl;
private PowerManager powerManager;
private WifiManager wifiManager;
private LockManager lockManager;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private PowerManager.WakeLock wakeLock;
private WifiManager.WifiLock wifiLock;
private PlayerEventListener activityListener;
private IBinder mBinder;
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private static final int NOTIFICATION_ID = 123789;
private static final int NOTIFICATION_ID = 123789;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView;
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
private boolean shouldUpdateOnProgress;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@ -92,12 +99,14 @@ public class BackgroundPlayer extends Service {
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate() called");
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
powerManager = ((PowerManager) getSystemService(POWER_SERVICE));
wifiManager = ((WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE));
lockManager = new LockManager(this);
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup();
mBinder = new PlayerServiceBinder(basePlayerImpl);
shouldUpdateOnProgress = true;
}
@Override
@ -110,51 +119,60 @@ public class BackgroundPlayer extends Service {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "destroy() called");
releaseWifiAndCpu();
stopForeground(true);
if (basePlayerImpl != null) basePlayerImpl.destroy();
onClose();
}
@Override
public IBinder onBind(Intent intent) {
return null;
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
public void openControl(final Context context) {
final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
private void onClose() {
if (basePlayerImpl != null) basePlayerImpl.destroyPlayer();
if (DEBUG) Log.d(TAG, "onClose() called");
if (lockManager != null) {
lockManager.releaseWifiAndCpu();
}
if (basePlayerImpl != null) {
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
mBinder = null;
basePlayerImpl = null;
lockManager = null;
stopForeground(true);
releaseWifiAndCpu();
stopSelf();
}
private void onScreenOnOff(boolean on) {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
if (on) {
if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning.get()) basePlayerImpl.startProgressLoop();
} else basePlayerImpl.stopProgressLoop();
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
notBuilder = createNotification();
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
@ -173,8 +191,6 @@ public class BackgroundPlayer extends Service {
}
private void setupNotification(RemoteViews remoteViews) {
//if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName());
@ -183,26 +199,16 @@ public class BackgroundPlayer extends Service {
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_CONTROLS), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
switch (basePlayerImpl.getCurrentRepeatMode()) {
case REPEAT_DISABLED:
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
break;
case REPEAT_ONE:
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode());
}
/**
@ -211,8 +217,8 @@ public class BackgroundPlayer extends Service {
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
private void updateNotification(int drawableId) {
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
private synchronized void updateNotification(int drawableId) {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
if (notBuilder == null) return;
if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
@ -234,136 +240,103 @@ public class BackgroundPlayer extends Service {
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void lockWifiAndCpu() {
if (DEBUG) Log.d(TAG, "lockWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return;
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
final String methodName = "setImageResource";
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
if (wakeLock != null) wakeLock.acquire();
if (wifiLock != null) wifiLock.acquire();
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_all);
break;
}
}
private void releaseWifiAndCpu() {
if (DEBUG) Log.d(TAG, "releaseWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
if (wifiLock != null && wifiLock.isHeld()) wifiLock.release();
wakeLock = null;
wifiLock = null;
}
//////////////////////////////////////////////////////////////////////////
private class BasePlayerImpl extends BasePlayer {
protected class BasePlayerImpl extends BasePlayer {
BasePlayerImpl(Context context) {
super(context);
}
@Override
public void handleIntent(Intent intent) {
public void handleIntent(final Intent intent) {
super.handleIntent(intent);
Serializable serializable = intent.getSerializableExtra(BackgroundPlayer.AUDIO_STREAM);
if (serializable instanceof AudioStream) audioStream = (AudioStream) serializable;
playUrl(audioStream.url, MediaFormat.getSuffixById(audioStream.format), true);
resetNotification();
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void initThumbnail() {
public void initThumbnail(final String url) {
resetNotification();
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
updateNotification(-1);
super.initThumbnail();
super.initThumbnail(url);
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
if (thumbnail != null) {
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
resetNotification();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
updateNotification(-1);
}
}
@Override
public void playUrl(String url, String format, boolean autoPlay) {
super.playUrl(url, format, autoPlay);
notBuilder = createNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void onPrepared(boolean playWhenReady) {
super.onPrepared(playWhenReady);
if (simpleExoPlayer.getDuration() < 15000) {
FAST_FORWARD_REWIND_AMOUNT = 2000;
} else if (simpleExoPlayer.getDuration() > 60 * 60 * 1000) {
FAST_FORWARD_REWIND_AMOUNT = 60000;
} else {
FAST_FORWARD_REWIND_AMOUNT = 10000;
}
PROGRESS_LOOP_INTERVAL = 1000;
basePlayerImpl.getPlayer().setVolume(1f);
simpleExoPlayer.setVolume(1f);
}
@Override
public void onRepeatClicked() {
super.onRepeatClicked();
int opacity = 255;
switch (currentRepeatMode) {
case REPEAT_DISABLED:
opacity = 77;
break;
case REPEAT_ONE:
opacity = 255;
break;
case REPEAT_ALL:
// Waiting :)
break;
}
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
updateNotification(-1);
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
if (bigNotRemoteView != null) bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress) return;
resetNotification();
if (bigNotRemoteView != null) {
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
}
updateNotification(-1);
}
@Override
public void onFastRewind() {
super.onFastRewind();
public void onPlayPrevious() {
super.onPlayPrevious();
triggerProgressUpdate();
}
@Override
public void onFastForward() {
super.onFastForward();
public void onPlayNext() {
super.onPlayNext();
triggerProgressUpdate();
}
@Override
public void onLoadingChanged(boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(int i) {
}
@Override
public void destroy() {
super.destroy();
@ -371,10 +344,96 @@ public class BackgroundPlayer extends Service {
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onError(Exception exception) {
exception.printStackTrace();
stopSelf();
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
@Override
public void onLoadingChanged(boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(int i) {
resetNotification();
updateNotification(-1);
updatePlayback();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
resetNotification();
updateNotification(-1);
updateMetadata();
}
@Override
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
if (index < 0) return null;
final AudioStream audio = info.audio_streams.get(index);
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
}
@Override
public void shutdown() {
super.shutdown();
onClose();
}
/*//////////////////////////////////////////////////////////////////////////
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
}
private void updateMetadata() {
if (activityListener != null && currentInfo != null) {
activityListener.onMetadataUpdate(currentInfo);
}
}
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters());
}
}
private void updateProgress(int currentProgress, int duration, int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
}
private void stopActivityBinding() {
if (activityListener != null) {
activityListener.onServiceStopped();
activityListener = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -386,10 +445,10 @@ public class BackgroundPlayer extends Service {
super.setupBroadcastReceiver(intentFilter);
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_PLAY_PAUSE);
intentFilter.addAction(ACTION_OPEN_DETAIL);
intentFilter.addAction(ACTION_OPEN_CONTROLS);
intentFilter.addAction(ACTION_REPEAT);
intentFilter.addAction(ACTION_FAST_FORWARD);
intentFilter.addAction(ACTION_FAST_REWIND);
intentFilter.addAction(ACTION_PLAY_PREVIOUS);
intentFilter.addAction(ACTION_PLAY_NEXT);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
@ -400,6 +459,7 @@ public class BackgroundPlayer extends Service {
@Override
public void onBroadcastReceived(Intent intent) {
super.onBroadcastReceived(intent);
if (intent == null || intent.getAction() == null) return;
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
switch (intent.getAction()) {
case ACTION_CLOSE:
@ -408,17 +468,17 @@ public class BackgroundPlayer extends Service {
case ACTION_PLAY_PAUSE:
onVideoPlayPause();
break;
case ACTION_OPEN_DETAIL:
onOpenDetail(BackgroundPlayer.this, basePlayerImpl.getVideoUrl(), basePlayerImpl.getVideoTitle());
case ACTION_OPEN_CONTROLS:
openControl(getApplicationContext());
break;
case ACTION_REPEAT:
onRepeatClicked();
break;
case ACTION_FAST_REWIND:
onFastRewind();
case ACTION_PLAY_NEXT:
onPlayNext();
break;
case ACTION_FAST_FORWARD:
onFastForward();
case ACTION_PLAY_PREVIOUS:
onPlayPrevious();
break;
case Intent.ACTION_SCREEN_ON:
onScreenOnOff(true);
@ -434,8 +494,14 @@ public class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoading() {
super.onLoading();
public void changeState(int state) {
super.changeState(state);
updatePlayback();
}
@Override
public void onBlocked() {
super.onBlocked();
setControlsOpacity(77);
updateNotification(-1);
@ -448,7 +514,7 @@ public class BackgroundPlayer extends Service {
setControlsOpacity(255);
updateNotification(R.drawable.ic_pause_white);
lockWifiAndCpu();
lockManager.acquireWifiAndCpu();
}
@Override
@ -456,9 +522,9 @@ public class BackgroundPlayer extends Service {
super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white);
if (isProgressLoopRunning.get()) stopProgressLoop();
if (isProgressLoopRunning()) stopProgressLoop();
releaseWifiAndCpu();
lockManager.releaseWifiAndCpu();
}
@Override
@ -466,11 +532,13 @@ public class BackgroundPlayer extends Service {
super.onCompleted();
setControlsOpacity(255);
resetNotification();
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
updateNotification(R.drawable.ic_replay_white);
releaseWifiAndCpu();
lockManager.releaseWifiAndCpu();
}
}
}

View File

@ -0,0 +1,39 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import org.schabi.newpipe.R;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "BackgroundPlayerActivity";
@Override
public String getTag() {
return TAG;
}
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_background_player);
}
@Override
public Intent getBindIntent() {
return new Intent(this, BackgroundPlayer.class);
}
@Override
public void startPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,10 @@ import android.graphics.Color;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
@ -35,16 +38,28 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
@ -52,11 +67,10 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
*
* @author mauriciocolli
*/
public class MainVideoPlayer extends Activity {
public final class MainVideoPlayer extends Activity {
private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private AudioManager audioManager;
private GestureDetector gestureDetector;
private boolean activityPaused;
@ -73,7 +87,6 @@ public class MainVideoPlayer extends Activity {
ThemeHelper.setTheme(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (getIntent() == null) {
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
@ -83,7 +96,7 @@ public class MainVideoPlayer extends Activity {
showSystemUi();
setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl();
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content));
playerImpl.handleIntent(getIntent());
}
@ -107,8 +120,10 @@ public class MainVideoPlayer extends Activity {
super.onStop();
if (DEBUG) Log.d(TAG, "onStop() called");
activityPaused = true;
if (playerImpl.getPlayer() != null) {
playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition());
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
playerImpl.setRecovery();
playerImpl.destroyPlayer();
}
}
@ -120,7 +135,10 @@ public class MainVideoPlayer extends Activity {
if (activityPaused) {
playerImpl.initPlayer();
playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white);
playerImpl.play(false);
playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying);
playerImpl.initPlayback(playerImpl.playQueue);
activityPaused = false;
}
}
@ -138,6 +156,7 @@ public class MainVideoPlayer extends Activity {
private void showSystemUi() {
if (DEBUG) Log.d(TAG, "showSystemUi() called");
if (playerImpl != null && playerImpl.queueVisible) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
@ -168,6 +187,29 @@ public class MainVideoPlayer extends Activity {
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
}
protected void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
break;
}
}
protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) {
final int shuffleAlpha = shuffled ? 255 : 77;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
shuffleButton.setImageAlpha(shuffleAlpha);
} else {
shuffleButton.setAlpha(shuffleAlpha);
}
}
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"})
@ -176,13 +218,24 @@ public class MainVideoPlayer extends Activity {
private TextView channelTextView;
private TextView volumeTextView;
private TextView brightnessTextView;
private ImageButton queueButton;
private ImageButton repeatButton;
private ImageButton shuffleButton;
private ImageButton screenRotationButton;
private ImageButton playPauseButton;
private ImageButton playPreviousButton;
private ImageButton playNextButton;
VideoPlayerImpl() {
super("VideoPlayerImpl" + MainVideoPlayer.TAG, MainVideoPlayer.this);
private RelativeLayout queueLayout;
private ImageButton itemsListCloseButton;
private RecyclerView itemsList;
private ItemTouchHelper itemTouchHelper;
private boolean queueVisible;
VideoPlayerImpl(final Context context) {
super("VideoPlayerImpl" + MainVideoPlayer.TAG, context);
}
@Override
@ -192,16 +245,17 @@ public class MainVideoPlayer extends Activity {
this.channelTextView = rootView.findViewById(R.id.channelTextView);
this.volumeTextView = rootView.findViewById(R.id.volumeTextView);
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView);
this.queueButton = rootView.findViewById(R.id.queueButton);
this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.shuffleButton = rootView.findViewById(R.id.shuffleButton);
this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton);
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
this.playNextButton = rootView.findViewById(R.id.playNextButton);
// Due to a bug on lower API, lets set the alpha instead of using a drawable
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
else { //noinspection deprecation
repeatButton.setAlpha(77);
}
titleTextView.setSelected(true);
channelTextView.setSelected(true);
getRootView().setKeepScreenOn(true);
}
@ -213,30 +267,63 @@ public class MainVideoPlayer extends Activity {
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
gestureDetector = new GestureDetector(context, listener);
gestureDetector.setIsLongpressEnabled(false);
playerImpl.getRootView().setOnTouchListener(listener);
getRootView().setOnTouchListener(listener);
queueButton.setOnClickListener(this);
repeatButton.setOnClickListener(this);
shuffleButton.setOnClickListener(this);
playPauseButton.setOnClickListener(this);
playPreviousButton.setOnClickListener(this);
playNextButton.setOnClickListener(this);
screenRotationButton.setOnClickListener(this);
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleIntent(Intent intent) {
super.handleIntent(intent);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
public void onRepeatModeChanged(int i) {
super.onRepeatModeChanged(i);
updatePlaybackButtons();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void shutdown() {
super.shutdown();
finish();
}
@Override
public void playUrl(String url, String format, boolean autoPlay) {
super.playUrl(url, format, autoPlay);
playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white);
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
playPauseButton.setImageResource(R.drawable.ic_pause_white);
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlaybackButtons();
}
/*//////////////////////////////////////////////////////////////////////////
// Player Overrides
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked();
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
if (playerImpl.getPlayer() == null) return;
if (simpleExoPlayer == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !PermissionHelper.checkSystemAlertWindowPermission(MainVideoPlayer.this)) {
@ -244,48 +331,55 @@ public class MainVideoPlayer extends Activity {
return;
}
context.startService(NavigationHelper.getOpenVideoPlayerIntent(context, PopupVideoPlayer.class, playerImpl));
if (playerImpl != null) playerImpl.destroyPlayer();
setRecovery();
final Intent intent = NavigationHelper.getPlayerIntent(
context,
PopupVideoPlayer.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackQuality()
);
context.startService(intent);
((View) getControlAnimationView().getParent()).setVisibility(View.GONE);
MainVideoPlayer.this.finish();
}
@Override
@SuppressWarnings("deprecation")
public void onRepeatClicked() {
super.onRepeatClicked();
if (DEBUG) Log.d(TAG, "onRepeatClicked() called");
switch (getCurrentRepeatMode()) {
case REPEAT_DISABLED:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
else repeatButton.setAlpha(77);
break;
case REPEAT_ONE:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(255);
else repeatButton.setAlpha(255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
destroy();
finish();
}
@Override
public void onClick(View v) {
super.onClick(v);
if (v.getId() == repeatButton.getId()) onRepeatClicked();
else if (v.getId() == playPauseButton.getId()) onVideoPlayPause();
else if (v.getId() == screenRotationButton.getId()) onScreenRotationClicked();
if (v.getId() == playPauseButton.getId()) {
onVideoPlayPause();
} else if (v.getId() == playPreviousButton.getId()) {
onPlayPrevious();
} else if (v.getId() == playNextButton.getId()) {
onPlayNext();
} else if (v.getId() == screenRotationButton.getId()) {
onScreenRotationClicked();
} else if (v.getId() == queueButton.getId()) {
onQueueClicked();
return;
} else if (v.getId() == repeatButton.getId()) {
onRepeatClicked();
return;
} else if (v.getId() == shuffleButton.getId()) {
onShuffleClicked();
return;
}
if (getCurrentState() != STATE_COMPLETED) {
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() {
animateView(getControlsRoot(), true, 300, 0, new Runnable() {
@Override
public void run() {
if (getCurrentState() == STATE_PLAYING && !playerImpl.isSomePopupMenuVisible()) {
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
}
}
@ -293,6 +387,24 @@ public class MainVideoPlayer extends Activity {
}
}
private void onQueueClicked() {
queueVisible = true;
hideSystemUi();
buildQueue();
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE);
queueLayout.setVisibility(View.VISIBLE);
itemsList.smoothScrollToPosition(playQueue.getIndex());
}
private void onQueueClosed() {
queueLayout.setVisibility(View.GONE);
queueVisible = false;
}
private void onScreenRotationClicked() {
if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called");
toggleOrientation();
@ -301,7 +413,7 @@ public class MainVideoPlayer extends Activity {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (playerImpl.wasPlaying()) {
if (wasPlaying()) {
hideControls(100, 0);
}
}
@ -313,28 +425,38 @@ public class MainVideoPlayer extends Activity {
}
@Override
public void onError(Exception exception) {
exception.printStackTrace();
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
finish();
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
return ListHelper.getDefaultResolutionIndex(context, sortedVideos);
}
@Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality) {
return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality);
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
private void animatePlayButtons(final boolean show, final int duration) {
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration);
animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration);
animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration);
}
@Override
public void onLoading() {
super.onLoading();
public void onBlocked() {
super.onBlocked();
playPauseButton.setImageResource(R.drawable.ic_pause_white);
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true);
}
@Override
public void onBuffering() {
super.onBuffering();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true);
}
@ -345,7 +467,7 @@ public class MainVideoPlayer extends Activity {
@Override
public void run() {
playPauseButton.setImageResource(R.drawable.ic_pause_white);
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 200);
animatePlayButtons(true, 200);
}
});
showSystemUi();
@ -359,7 +481,7 @@ public class MainVideoPlayer extends Activity {
@Override
public void run() {
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 200);
animatePlayButtons(true, 200);
}
});
@ -370,25 +492,22 @@ public class MainVideoPlayer extends Activity {
@Override
public void onPausedSeek() {
super.onPausedSeek();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 100);
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true);
}
@Override
public void onCompleted() {
if (getCurrentRepeatMode() == RepeatMode.REPEAT_ONE) {
playPauseButton.setImageResource(R.drawable.ic_pause_white);
} else {
showSystemUi();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, new Runnable() {
@Override
public void run() {
playPauseButton.setImageResource(R.drawable.ic_replay_white);
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
}
});
}
showSystemUi();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, new Runnable() {
@Override
public void run() {
playPauseButton.setImageResource(R.drawable.ic_replay_white);
animatePlayButtons(true, 300);
}
});
getRootView().setKeepScreenOn(false);
super.onCompleted();
}
@ -397,6 +516,20 @@ public class MainVideoPlayer extends Activity {
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showControlsThenHide() {
if (queueVisible) return;
super.showControlsThenHide();
}
@Override
public void showControls(long duration) {
if (queueVisible) return;
super.showControls(duration);
}
@Override
public void hideControls(final long duration, long delay) {
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
@ -414,6 +547,86 @@ public class MainVideoPlayer extends Activity {
}, delay);
}
private void updatePlaybackButtons() {
if (repeatButton == null || shuffleButton == null ||
simpleExoPlayer == null || playQueue == null) return;
setRepeatModeButton(repeatButton, getRepeatMode());
setShuffleButton(shuffleButton, playQueue.isShuffled());
}
private void buildQueue() {
queueLayout = findViewById(R.id.playQueuePanel);
itemsListCloseButton = findViewById(R.id.playQueueClose);
itemsList = findViewById(R.id.playQueue);
itemsList.setAdapter(playQueueAdapter);
itemsList.setClickable(true);
itemsList.setLongClickable(true);
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
playQueueAdapter.setSelectedListener(getOnSelectedListener());
itemsListCloseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onQueueClosed();
}
});
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
@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();
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) {}
};
}
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item, View view) {
onSelected(item);
}
@Override
public void held(PlayQueueItem item, View view) {
final int index = playQueue.indexOf(item);
if (index != -1) playQueue.remove(index);
}
@Override
public void onStartDrag(PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
};
}
///////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////
@ -441,11 +654,6 @@ public class MainVideoPlayer extends Activity {
public ImageButton getPlayPauseButton() {
return playPauseButton;
}
@Override
public void onRepeatModeChanged(int i) {
}
}
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
@ -455,15 +663,20 @@ public class MainVideoPlayer extends Activity {
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (!playerImpl.isPlaying()) return false;
if (e.getX() > playerImpl.getRootView().getWidth() / 2) playerImpl.onFastForward();
else playerImpl.onFastRewind();
if (e.getX() > playerImpl.getRootView().getWidth() / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl.getCurrentState() != BasePlayer.STATE_PLAYING) return true;
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0);
else {
@ -473,12 +686,12 @@ public class MainVideoPlayer extends Activity {
return true;
}
private final boolean isGestureControlsEnabled = playerImpl.getSharedPreferences().getBoolean(getString(R.string.player_gesture_controls_key), true);
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
private float currentBrightness = .5f;
private int currentVolume, maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume();
private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0;
private final String brightnessUnicode = new String(Character.toChars(0x2600));
@ -492,7 +705,7 @@ public class MainVideoPlayer extends Activity {
// TODO: Improve video gesture controls
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isGestureControlsEnabled) return false;
if (!isPlayerGestureEnabled) return false;
//noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
@ -513,13 +726,15 @@ public class MainVideoPlayer extends Activity {
if (e1.getX() > playerImpl.getRootView().getWidth() / 2) {
double floor = Math.floor(up ? stepVolume : -stepVolume);
currentVolume = (int) (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + floor);
currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor);
if (currentVolume >= maxVolume) currentVolume = maxVolume;
if (currentVolume <= minVolume) currentVolume = (int) minVolume;
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0);
playerImpl.getAudioReactor().setVolume(currentVolume);
currentVolume = playerImpl.getAudioReactor().getVolume();
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
playerImpl.getVolumeTextView().setText(volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%");
final String volumeText = volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%";
playerImpl.getVolumeTextView().setText(volumeText);
if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200);
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);
@ -534,7 +749,8 @@ public class MainVideoPlayer extends Activity {
if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness);
int brightnessNormalized = Math.round(currentBrightness * 100);
playerImpl.getBrightnessTextView().setText(brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%");
final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%";
playerImpl.getBrightnessTextView().setText(brightnessText);
if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200);
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);

View File

@ -0,0 +1,16 @@
package org.schabi.newpipe.player;
import android.os.Binder;
import android.support.annotation.NonNull;
class PlayerServiceBinder extends Binder {
private final BasePlayer basePlayer;
PlayerServiceBinder(@NonNull final BasePlayer basePlayer) {
this.basePlayer = basePlayer;
}
BasePlayer getPlayerInstance() {
return basePlayer;
}
}

View File

@ -35,6 +35,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.DisplayMetrics;
import android.util.Log;
@ -49,23 +50,25 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
@ -75,13 +78,14 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
@ -89,7 +93,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
*
* @author mauriciocolli
*/
public class PopupVideoPlayer extends Service {
public final class PopupVideoPlayer extends Service {
private static final String TAG = ".PopupVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int SHUTDOWN_FLING_VELOCITY = 10000;
@ -97,7 +101,7 @@ public class PopupVideoPlayer extends Service {
private static final int NOTIFICATION_ID = 40028922;
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL";
public static final String ACTION_OPEN_CONTROLS = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_CONTROLS";
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT";
private static final String POPUP_SAVED_WIDTH = "popup_saved_width";
@ -114,17 +118,19 @@ public class PopupVideoPlayer extends Service {
private float minimumWidth, minimumHeight;
private float maximumWidth, maximumHeight;
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private ImageLoader imageLoader = ImageLoader.getInstance();
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
private VideoPlayerImpl playerImpl;
private Disposable currentWorker;
private LockManager lockManager;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private PlayerEventListener activityListener;
private IBinder mBinder;
/*//////////////////////////////////////////////////////////////////////////
// Service LifeCycle
@ -135,20 +141,21 @@ public class PopupVideoPlayer extends Service {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
playerImpl = new VideoPlayerImpl();
lockManager = new LockManager(this);
playerImpl = new VideoPlayerImpl(this);
ThemeHelper.setTheme(this);
mBinder = new PlayerServiceBinder(playerImpl);
}
@Override
@SuppressWarnings("unchecked")
public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG)
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (playerImpl.getPlayer() == null) initPopup();
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
if (imageLoader != null) imageLoader.clearMemoryCache();
if (intent.getStringExtra(Constants.KEY_URL) != null) {
if (intent != null && intent.getStringExtra(Constants.KEY_URL) != null) {
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
final String url = intent.getStringExtra(Constants.KEY_URL);
@ -186,19 +193,12 @@ public class PopupVideoPlayer extends Service {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy() called");
stopForeground(true);
if (playerImpl != null) {
playerImpl.destroy();
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
}
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (currentWorker != null) currentWorker.dispose();
savePositionAndSize();
onClose();
}
@Override
public IBinder onBind(Intent intent) {
return null;
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
@ -237,7 +237,6 @@ public class PopupVideoPlayer extends Service {
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
gestureDetector = new GestureDetector(this, listener);
//gestureDetector.setIsLongpressEnabled(false);
rootView.setOnTouchListener(listener);
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
@ -248,12 +247,13 @@ public class PopupVideoPlayer extends Service {
// Notification
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
notBuilder = createNotification();
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification);
if (playerImpl.getVideoThumbnail() == null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
@ -262,21 +262,11 @@ public class PopupVideoPlayer extends Service {
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_CONTROLS), PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
switch (playerImpl.getCurrentRepeatMode()) {
case REPEAT_DISABLED:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
break;
case REPEAT_ONE:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode());
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
@ -302,21 +292,33 @@ public class PopupVideoPlayer extends Service {
// Misc
//////////////////////////////////////////////////////////////////////////*/
public void onVideoClose() {
if (DEBUG) Log.d(TAG, "onVideoClose() called");
public void onClose() {
if (DEBUG) Log.d(TAG, "onClose() called");
if (playerImpl != null) {
if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView());
playerImpl.setRootView(null);
}
playerImpl.stopActivityBinding();
playerImpl.destroy();
}
if (lockManager != null) lockManager.releaseWifiAndCpu();
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (currentWorker != null) currentWorker.dispose();
mBinder = null;
playerImpl = null;
stopForeground(true);
stopSelf();
}
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(i);
public void openControl(final Context context) {
final Intent intent = new Intent(context, PopupVideoPlayerActivity.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
@ -364,7 +366,7 @@ public class PopupVideoPlayer extends Service {
}
private void updatePopupSize(int width, int height) {
//if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]");
if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]");
width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width);
@ -380,25 +382,39 @@ public class PopupVideoPlayer extends Service {
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
}
protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) {
final String methodName = "setImageResource";
if (remoteViews == null) return;
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_all);
break;
}
}
///////////////////////////////////////////////////////////////////////////
private class VideoPlayerImpl extends VideoPlayer {
protected class VideoPlayerImpl extends VideoPlayer {
private TextView resizingIndicator;
VideoPlayerImpl() {
super("VideoPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this);
@Override
public void handleIntent(Intent intent) {
super.handleIntent(intent);
resetNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void playUrl(String url, String format, boolean autoPlay) {
super.playUrl(url, format, autoPlay);
windowLayoutParams.width = (int) popupWidth;
windowLayoutParams.height = (int) getMinimumVideoHeight(popupWidth);
windowManager.updateViewLayout(getRootView(), windowLayoutParams);
notBuilder = createNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
VideoPlayerImpl(final Context context) {
super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context);
}
@Override
@ -409,58 +425,53 @@ public class PopupVideoPlayer extends Service {
@Override
public void destroy() {
super.destroy();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
super.destroy();
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
if (thumbnail != null) {
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
notBuilder = createNotification();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
updateNotification(-1);
}
}
@Override
public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked();
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
setRecovery();
Intent intent;
if (!getSharedPreferences().getBoolean(getResources().getString(R.string.use_old_player_key), false)) {
intent = NavigationHelper.getOpenVideoPlayerIntent(context, MainVideoPlayer.class, playerImpl);
if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false);
if (!isUsingOldPlayer(getApplicationContext())) {
intent = NavigationHelper.getPlayerIntent(
context,
MainVideoPlayer.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackQuality()
);
if (!isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} else {
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString())
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedVideoStream().url)
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
if (playerImpl != null) playerImpl.destroyPlayer();
stopSelf();
}
@Override
public void onRepeatClicked() {
super.onRepeatClicked();
switch (getCurrentRepeatMode()) {
case REPEAT_DISABLED:
// Drawable didn't work on low API :/
//notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white);
// Set the icon to 30% opacity - 255 (max) * .3
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
break;
case REPEAT_ONE:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
updateNotification(-1);
onClose();
}
@Override
@ -470,20 +481,112 @@ public class PopupVideoPlayer extends Service {
}
@Override
public void onError(Exception exception) {
exception.printStackTrace();
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
stopSelf();
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (wasPlaying()) {
hideControls(100, 0);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (playerImpl.wasPlaying()) {
hideControls(100, 0);
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
updateProgress(currentProgress, duration, bufferPercent);
super.onUpdateProgress(currentProgress, duration, bufferPercent);
}
@Override
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
}
@Override
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality) {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality);
}
/*//////////////////////////////////////////////////////////////////////////
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
}
private void updateMetadata() {
if (activityListener != null && currentInfo != null) {
activityListener.onMetadataUpdate(currentInfo);
}
}
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
}
}
private void updateProgress(int currentProgress, int duration, int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
}
private void stopActivityBinding() {
if (activityListener != null) {
activityListener.onServiceStopped();
activityListener = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onRepeatModeChanged(int i) {
super.onRepeatModeChanged(i);
setRepeatModeRemote(notRemoteView, i);
updateNotification(-1);
updatePlayback();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) {
super.sync(item, info);
updateMetadata();
}
@Override
public void shutdown() {
super.shutdown();
onClose();
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast Receiver
//////////////////////////////////////////////////////////////////////////*/
@ -494,36 +597,53 @@ public class PopupVideoPlayer extends Service {
if (DEBUG) Log.d(TAG, "setupBroadcastReceiver() called with: intentFilter = [" + intentFilter + "]");
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_PLAY_PAUSE);
intentFilter.addAction(ACTION_OPEN_DETAIL);
intentFilter.addAction(ACTION_OPEN_CONTROLS);
intentFilter.addAction(ACTION_REPEAT);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
}
@Override
public void onBroadcastReceived(Intent intent) {
super.onBroadcastReceived(intent);
if (intent == null || intent.getAction() == null) return;
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
switch (intent.getAction()) {
case ACTION_CLOSE:
onVideoClose();
onClose();
break;
case ACTION_PLAY_PAUSE:
playerImpl.onVideoPlayPause();
onVideoPlayPause();
break;
case ACTION_OPEN_DETAIL:
onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle());
case ACTION_OPEN_CONTROLS:
openControl(getApplicationContext());
break;
case ACTION_REPEAT:
playerImpl.onRepeatClicked();
onRepeatClicked();
break;
case Intent.ACTION_SCREEN_ON:
enableVideoRenderer(true);
break;
case Intent.ACTION_SCREEN_OFF:
enableVideoRenderer(false);
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoading() {
super.onLoading();
public void changeState(int state) {
super.changeState(state);
updatePlayback();
}
@Override
public void onBlocked() {
super.onBlocked();
updateNotification(R.drawable.ic_play_arrow_white);
}
@ -531,6 +651,7 @@ public class PopupVideoPlayer extends Service {
public void onPlaying() {
super.onPlaying();
updateNotification(R.drawable.ic_pause_white);
lockManager.acquireWifiAndCpu();
}
@Override
@ -544,6 +665,7 @@ public class PopupVideoPlayer extends Service {
super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white);
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
lockManager.releaseWifiAndCpu();
}
@Override
@ -557,17 +679,28 @@ public class PopupVideoPlayer extends Service {
super.onCompleted();
updateNotification(R.drawable.ic_replay_white);
showAndAnimateControl(R.drawable.ic_replay_white, false);
lockManager.releaseWifiAndCpu();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void enableVideoRenderer(final boolean enable) {
final int videoRendererIndex = getVideoRendererIndex();
if (trackSelector != null && videoRendererIndex != -1) {
trackSelector.setRendererDisabled(videoRendererIndex, !enable);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@SuppressWarnings("WeakerAccess")
public TextView getResizingIndicator() {
return resizingIndicator;
}
@Override
public void onRepeatModeChanged(int i) {
}
}
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
@ -580,16 +713,21 @@ public class PopupVideoPlayer extends Service {
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG)
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (!playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
else playerImpl.onFastRewind();
if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false;
if (e.getX() > popupWidth / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl.getPlayer() == null) return false;
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
playerImpl.onVideoPlayPause();
return true;
}
@ -614,7 +752,7 @@ public class PopupVideoPlayer extends Service {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (isResizing) return super.onScroll(e1, e2, distanceX, distanceY);
if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, distanceY);
if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING
&& (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0);
@ -645,6 +783,7 @@ public class PopupVideoPlayer extends Service {
private void onScrollEnd() {
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
if (playerImpl == null) return;
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
}
@ -652,9 +791,10 @@ public class PopupVideoPlayer extends Service {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (playerImpl == null) return false;
if (Math.abs(velocityX) > SHUTDOWN_FLING_VELOCITY) {
if (DEBUG) Log.d(TAG, "Popup close fling velocity= " + velocityX);
onVideoClose();
onClose();
return true;
}
return false;
@ -663,6 +803,7 @@ public class PopupVideoPlayer extends Service {
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
if (playerImpl == null) return false;
if (event.getPointerCount() == 2 && !isResizing) {
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
playerImpl.showAndAnimateControl(-1, true);
@ -739,49 +880,11 @@ public class PopupVideoPlayer extends Service {
this.serviceId = serviceId;
}
public void onReceive(StreamInfo info) {
playerImpl.setVideoTitle(info.name);
playerImpl.setVideoUrl(info.url);
playerImpl.setVideoThumbnailUrl(info.thumbnail_url);
playerImpl.setUploaderName(info.uploader_name);
playerImpl.setVideoStreamsList(new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)));
playerImpl.setAudioStream(ListHelper.getHighestQualityAudio(info.audio_streams));
int defaultResolution = ListHelper.getPopupDefaultResolutionIndex(context, playerImpl.getVideoStreamsList());
playerImpl.setSelectedIndexStream(defaultResolution);
if (DEBUG) {
Log.d(TAG, "FetcherHandler.StreamExtractor: chosen = "
+ MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " "
+ info.video_streams.get(defaultResolution).resolution + " > "
+ info.video_streams.get(defaultResolution).url);
}
if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000);
else playerImpl.setVideoStartPos(-1);
/*package-private*/ void onReceive(final StreamInfo info) {
mainHandler.post(new Runnable() {
@Override
public void run() {
playerImpl.play(true);
}
});
imageLoader.resume();
imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) {
if (playerImpl == null || playerImpl.getPlayer() == null) return;
if (DEBUG) Log.d(TAG, "FetcherHandler.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
mainHandler.post(new Runnable() {
@Override
public void run() {
playerImpl.setVideoThumbnail(loadedImage);
if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
updateNotification(-1);
}
});
playerImpl.initPlayback(new SinglePlayQueue(info));
}
});
}
@ -812,7 +915,7 @@ public class PopupVideoPlayer extends Service {
stopSelf();
}
public void onReCaptchaException() {
/*package-private*/ void onReCaptchaException() {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
Intent intent = new Intent(context, ReCaptchaActivity.class);

View File

@ -0,0 +1,39 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import org.schabi.newpipe.R;
public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "PopupVideoPlayerActivity";
@Override
public String getTag() {
return TAG;
}
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_popup_player);
}
@Override
public Intent getBindIntent() {
return new Intent(this, PopupVideoPlayer.class);
}
@Override
public void startPlayerListener() {
if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) {
((PopupVideoPlayer.VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) {
((PopupVideoPlayer.VideoPlayerImpl) player).removeActivityListener(this);
}
}
}

View File

@ -0,0 +1,562 @@
package org.schabi.newpipe.player;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
private boolean serviceBound;
private ServiceConnection serviceConnection;
protected BasePlayer player;
private boolean seeking;
private boolean redraw;
////////////////////////////////////////////////////////////////////////////
// Views
////////////////////////////////////////////////////////////////////////////
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 View rootView;
private RecyclerView itemsList;
private ItemTouchHelper itemTouchHelper;
private LinearLayout metadata;
private TextView metadataTitle;
private TextView metadataArtist;
private SeekBar progressSeekBar;
private TextView progressCurrentTime;
private TextView progressEndTime;
private TextView seekDisplay;
private ImageButton repeatButton;
private ImageButton backwardButton;
private ImageButton playPauseButton;
private ImageButton forwardButton;
private ImageButton shuffleButton;
private ProgressBar progressBar;
private TextView playbackSpeedButton;
private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton;
private PopupMenu playbackPitchPopupMenu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
////////////////////////////////////////////////////////////////////////////
public abstract String getTag();
public abstract String getSupportActionTitle();
public abstract Intent getBindIntent();
public abstract void startPlayerListener();
public abstract void stopPlayerListener();
////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle
////////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
setContentView(R.layout.activity_player_queue_control);
rootView = findViewById(R.id.main_content);
final Toolbar toolbar = rootView.findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(getSupportActionTitle());
}
serviceConnection = getServiceConnection();
bind();
}
@Override
protected void onResume() {
super.onResume();
if (redraw) {
recreate();
redraw = false;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_play_queue, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_history:
NavigationHelper.openHistory(this);
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
redraw = true;
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbind();
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////
private void bind() {
final boolean success = bindService(getBindIntent(), serviceConnection, BIND_AUTO_CREATE);
if (!success) {
unbindService(serviceConnection);
}
serviceBound = success;
}
private void unbind() {
if(serviceBound) {
unbindService(serviceConnection);
serviceBound = false;
stopPlayerListener();
player = null;
}
}
private ServiceConnection getServiceConnection() {
return new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(getTag(), "Player service is disconnected");
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(getTag(), "Player service is connected");
if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance();
}
if (player == null || player.getPlayQueue() == null ||
player.getPlayQueueAdapter() == null || player.getPlayer() == null) {
unbind();
finish();
} else {
buildComponents();
startPlayerListener();
}
}
};
}
////////////////////////////////////////////////////////////////////////////
// Component Building
////////////////////////////////////////////////////////////////////////////
private void buildComponents() {
buildQueue();
buildMetadata();
buildSeekBar();
buildControls();
}
private void buildQueue() {
itemsList = findViewById(R.id.play_queue);
itemsList.setLayoutManager(new LinearLayoutManager(this));
itemsList.setAdapter(player.getPlayQueueAdapter());
itemsList.setClickable(true);
itemsList.setLongClickable(true);
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
}
private void buildMetadata() {
metadata = rootView.findViewById(R.id.metadata);
metadataTitle = rootView.findViewById(R.id.song_name);
metadataArtist = rootView.findViewById(R.id.artist_name);
metadata.setOnClickListener(this);
metadataTitle.setSelected(true);
metadataArtist.setSelected(true);
}
private void buildSeekBar() {
progressCurrentTime = rootView.findViewById(R.id.current_time);
progressSeekBar = rootView.findViewById(R.id.seek_bar);
progressEndTime = rootView.findViewById(R.id.end_time);
seekDisplay = rootView.findViewById(R.id.seek_display);
progressSeekBar.setOnSeekBarChangeListener(this);
}
private void buildControls() {
repeatButton = rootView.findViewById(R.id.control_repeat);
backwardButton = rootView.findViewById(R.id.control_backward);
playPauseButton = rootView.findViewById(R.id.control_play_pause);
forwardButton = rootView.findViewById(R.id.control_forward);
shuffleButton = rootView.findViewById(R.id.control_shuffle);
playbackSpeedButton = rootView.findViewById(R.id.control_playback_speed);
playbackPitchButton = rootView.findViewById(R.id.control_playback_pitch);
progressBar = rootView.findViewById(R.id.control_progress_bar);
repeatButton.setOnClickListener(this);
backwardButton.setOnClickListener(this);
playPauseButton.setOnClickListener(this);
forwardButton.setOnClickListener(this);
shuffleButton.setOnClickListener(this);
playbackSpeedButton.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(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
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(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
player.setPlaybackPitch(playbackPitch);
return true;
}
});
}
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) player.getPlayQueue().remove(index);
return true;
}
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true;
}
});
menu.show();
}
////////////////////////////////////////////////////////////////////////////
// Component Helpers
////////////////////////////////////////////////////////////////////////////
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
@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();
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) {}
};
}
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item, View view) {
player.onSelected(item);
}
@Override
public void held(PlayQueueItem item, View view) {
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) buildItemPopupMenu(item, view);
}
@Override
public void onStartDrag(PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
};
}
private void onOpenDetail(int serviceId, String videoUrl, String videoTitle) {
NavigationHelper.openVideoDetail(this, serviceId, videoUrl, videoTitle);
}
private void scrollToSelected() {
itemsList.smoothScrollToPosition(player.getPlayQueue().getIndex());
}
////////////////////////////////////////////////////////////////////////////
// Component On-Click Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onClick(View view) {
if (view.getId() == repeatButton.getId()) {
player.onRepeatClicked();
} else if (view.getId() == backwardButton.getId()) {
player.onPlayPrevious();
} else if (view.getId() == playPauseButton.getId()) {
player.onVideoPlayPause();
} else if (view.getId() == forwardButton.getId()) {
player.onPlayNext();
} else if (view.getId() == shuffleButton.getId()) {
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
playbackSpeedPopupMenu.show();
} else if (view.getId() == playbackPitchButton.getId()) {
playbackPitchPopupMenu.show();
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
}
}
////////////////////////////////////////////////////////////////////////////
// Seekbar Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
final String seekTime = Localization.getDurationString(progress / 1000);
progressCurrentTime.setText(seekTime);
seekDisplay.setText(seekTime);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
seeking = true;
seekDisplay.setVisibility(View.VISIBLE);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
player.simpleExoPlayer.seekTo(seekBar.getProgress());
seekDisplay.setVisibility(View.GONE);
seeking = false;
}
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) {
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
}
@Override
public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) {
// Set buffer progress
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() * ((float) bufferPercent / 100)));
// Set Duration
progressSeekBar.setMax(duration);
progressEndTime.setText(Localization.getDurationString(duration / 1000));
// Set current time if not seeking
if (!seeking) {
progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
}
}
@Override
public void onMetadataUpdate(StreamInfo info) {
if (info != null) {
metadataTitle.setText(info.name);
metadataArtist.setText(info.uploader_name);
scrollToSelected();
}
}
@Override
public void onServiceStopped() {
unbind();
finish();
}
////////////////////////////////////////////////////////////////////////////
// Binding Service Helper
////////////////////////////////////////////////////////////////////////////
private void onStateChanged(final int state) {
switch (state) {
case BasePlayer.STATE_PAUSED:
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
break;
case BasePlayer.STATE_PLAYING:
playPauseButton.setImageResource(R.drawable.ic_pause_white);
break;
case BasePlayer.STATE_COMPLETED:
playPauseButton.setImageResource(R.drawable.ic_replay_white);
break;
default:
break;
}
switch (state) {
case BasePlayer.STATE_PAUSED:
case BasePlayer.STATE_PLAYING:
case BasePlayer.STATE_COMPLETED:
playPauseButton.setClickable(true);
playPauseButton.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
break;
default:
playPauseButton.setClickable(false);
playPauseButton.setVisibility(View.INVISIBLE);
progressBar.setVisibility(View.VISIBLE);
break;
}
}
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_all);
break;
}
final int shuffleAlpha = shuffled ? 255 : 77;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
shuffleButton.setImageAlpha(shuffleAlpha);
} else {
shuffleButton.setAlpha(shuffleAlpha);
}
}
private void onPlaybackParameterChanged(final PlaybackParameters parameters) {
if (parameters != null) {
playbackSpeedButton.setText(formatSpeed(parameters.speed));
playbackPitchButton.setText(formatPitch(parameters.pitch));
}
}
}

View File

@ -29,9 +29,10 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Menu;
@ -45,9 +46,9 @@ import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
@ -55,14 +56,17 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
@ -79,24 +83,21 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// Intent
//////////////////////////////////////////////////////////////////////////*/
public static final String VIDEO_STREAMS_LIST = "video_streams_list";
public static final String VIDEO_ONLY_AUDIO_STREAM = "video_only_audio_stream";
public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream";
public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe";
private int selectedIndexStream;
private ArrayList<VideoStream> videoStreamsList = new ArrayList<>();
private AudioStream videoOnlyAudioStream;
/*//////////////////////////////////////////////////////////////////////////
// Player
//////////////////////////////////////////////////////////////////////////*/
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
private ArrayList<VideoStream> availableStreams;
private int selectedStreamIndex;
protected String playbackQuality;
private boolean startedFromNewPipe = true;
private boolean wasPlaying = false;
protected boolean wasPlaying = false;
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -119,7 +120,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private SeekBar playbackSeekBar;
private TextView playbackCurrentTime;
private TextView playbackEndTime;
private TextView playbackSpeed;
private TextView playbackSpeedTextView;
private View topControlsRoot;
private TextView qualityTextView;
@ -129,7 +130,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private Handler controlsVisibilityHandler = new Handler();
private boolean isSomePopupMenuVisible = false;
private boolean qualityChanged = false;
private int qualityPopupMenuGroupId = 69;
private PopupMenu qualityPopupMenu;
@ -162,7 +162,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed);
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls);
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
@ -175,7 +175,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeed);
this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView);
((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY);
@ -185,7 +185,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initListeners() {
super.initListeners();
playbackSeekBar.setOnSeekBarChangeListener(this);
playbackSpeed.setOnClickListener(this);
playbackSpeedTextView.setOnClickListener(this);
fullScreenButton.setOnClickListener(this);
qualityTextView.setOnClickListener(this);
}
@ -194,80 +194,103 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initPlayer() {
super.initPlayer();
simpleExoPlayer.setVideoSurfaceView(surfaceView);
simpleExoPlayer.setVideoListener(this);
simpleExoPlayer.addVideoListener(this);
if (Build.VERSION.SDK_INT >= 21) {
trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context));
}
}
@SuppressWarnings("unchecked")
public void handleIntent(Intent intent) {
super.handleIntent(intent);
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
@Override
public void handleIntent(final Intent intent) {
if (intent == null) return;
selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1);
Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST);
if (serializable instanceof ArrayList) videoStreamsList = (ArrayList<VideoStream>) serializable;
if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List<VideoStream>) serializable);
Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM);
if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream;
startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true);
play(true);
}
public void play(boolean autoPlay) {
playUrl(getSelectedVideoStream().url, MediaFormat.getSuffixById(getSelectedVideoStream().format), autoPlay);
}
@Override
public void playUrl(String url, String format, boolean autoPlay) {
if (DEBUG) Log.d(TAG, "play() called with: url = [" + url + "], autoPlay = [" + autoPlay + "]");
qualityChanged = false;
if (url == null || simpleExoPlayer == null) {
RuntimeException runtimeException = new RuntimeException((url == null ? "Url " : "Player ") + " null");
onError(runtimeException);
throw runtimeException;
if (intent.hasExtra(PLAYBACK_QUALITY)) {
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
}
super.handleIntent(intent);
}
/*//////////////////////////////////////////////////////////////////////////
// UI Builders
//////////////////////////////////////////////////////////////////////////*/
public void buildQualityMenu() {
if (qualityPopupMenu == null) return;
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
buildQualityMenu(qualityPopupMenu);
for (int i = 0; i < availableStreams.size(); i++) {
VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
}
qualityTextView.setText(getSelectedVideoStream().resolution);
qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this);
}
private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) return;
playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
buildPlaybackSpeedMenu(playbackSpeedPopupMenu);
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
}
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
playbackSpeedPopupMenu.setOnDismissListener(this);
}
super.playUrl(url, format, autoPlay);
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected abstract int getDefaultResolutionIndex(final List<VideoStream> sortedVideos);
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE);
if (info != null) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
availableStreams = new ArrayList<>(videos);
if (playbackQuality == null) {
selectedStreamIndex = getDefaultResolutionIndex(videos);
} else {
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
}
buildQualityMenu();
buildPlaybackSpeedMenu();
qualityTextView.setVisibility(View.VISIBLE);
playbackSpeedTextView.setVisibility(View.VISIBLE);
}
}
@Override
public MediaSource buildMediaSource(String url, String overrideExtension) {
MediaSource mediaSource = super.buildMediaSource(url, overrideExtension);
if (!getSelectedVideoStream().isVideoOnly || videoOnlyAudioStream == null) return mediaSource;
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
Uri audioUri = Uri.parse(videoOnlyAudioStream.url);
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
}
public void buildQualityMenu(PopupMenu popupMenu) {
for (int i = 0; i < videoStreamsList.size(); i++) {
VideoStream videoStream = videoStreamsList.get(i);
popupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
final VideoStream video;
if (playbackQuality == null) {
final int index = getDefaultResolutionIndex(videos);
video = videos.get(index);
} else {
final int index = getOverrideResolutionIndex(videos, getPlaybackQuality());
video = videos.get(index);
}
qualityTextView.setText(getSelectedVideoStream().resolution);
popupMenu.setOnMenuItemClickListener(this);
popupMenu.setOnDismissListener(this);
}
private void buildPlaybackSpeedMenu(PopupMenu popupMenu) {
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
popupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
}
playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
popupMenu.setOnMenuItemClickListener(this);
popupMenu.setOnDismissListener(this);
final MediaSource streamSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
if (!video.isVideoOnly || audio == null) return streamSource;
// Merge with audio stream in case if video does not contain audio
final MediaSource audioSource = buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
return new MergingMediaSource(streamSource, audioSource);
}
/*//////////////////////////////////////////////////////////////////////////
@ -275,18 +298,13 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoading() {
if (DEBUG) Log.d(TAG, "onLoading() called");
if (!isProgressLoopRunning.get()) startProgressLoop();
public void onBlocked() {
super.onBlocked();
controlsVisibilityHandler.removeCallbacksAndMessages(null);
animateView(controlsRoot, false, 300);
showAndAnimateControl(-1, true);
playbackSeekBar.setEnabled(true);
playbackSeekBar.setProgress(0);
playbackSeekBar.setEnabled(false);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
@ -299,12 +317,19 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
@Override
public void onPlaying() {
if (DEBUG) Log.d(TAG, "onPlaying() called");
if (!isProgressLoopRunning.get()) startProgressLoop();
super.onPlaying();
showAndAnimateControl(-1, true);
playbackSeekBar.setEnabled(true);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
loadingPanel.setVisibility(View.GONE);
showControlsThenHide();
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
animateView(endScreen, false, 0);
}
@Override
@ -329,30 +354,14 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
@Override
public void onCompleted() {
if (DEBUG) Log.d(TAG, "onCompleted() called");
if (isProgressLoopRunning.get()) stopProgressLoop();
super.onCompleted();
showControls(500);
animateView(endScreen, true, 800);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
loadingPanel.setVisibility(View.GONE);
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
playbackSeekBar.setProgress(playbackSeekBar.getMax());
playbackSeekBar.setEnabled(false);
playbackEndTime.setText(getTimeString(playbackSeekBar.getMax()));
playbackCurrentTime.setText(playbackEndTime.getText());
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
animateView(surfaceForeground, true, 100);
if (currentRepeatMode == RepeatMode.REPEAT_ONE) {
changeState(STATE_LOADING);
simpleExoPlayer.seekTo(0);
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -380,15 +389,9 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void onPrepared(boolean playWhenReady) {
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
if (videoStartPos > 0) {
playbackSeekBar.setProgress((int) videoStartPos);
playbackCurrentTime.setText(getTimeString((int) videoStartPos));
videoStartPos = -1;
}
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
super.onPrepared(playWhenReady);
}
@ -403,6 +406,10 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
if (!isPrepared) return;
if (duration != playbackSeekBar.getMax()) {
playbackEndTime.setText(getTimeString(duration));
playbackSeekBar.setMax(duration);
}
if (currentState != STATE_PAUSED) {
if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress);
playbackCurrentTime.setText(getTimeString(currentProgress));
@ -415,22 +422,17 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
}
}
@Override
public void onVideoPlayPauseRepeat() {
if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called");
if (qualityChanged) {
setVideoStartPos(0);
play(true);
} else super.onVideoPlayPauseRepeat();
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
if (thumbnail != null) endScreen.setImageBitmap(thumbnail);
}
protected abstract void onFullScreenButtonClicked();
protected void onFullScreenButtonClicked() {
if (!isPlayerReady()) return;
changeState(STATE_BLOCKED);
}
@Override
public void onFastRewind() {
@ -455,7 +457,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
onFullScreenButtonClicked();
} else if (v.getId() == qualityTextView.getId()) {
onQualitySelectorClicked();
} else if (v.getId() == playbackSpeed.getId()) {
} else if (v.getId() == playbackSpeedTextView.getId()) {
onPlaybackSpeedClicked();
}
}
@ -469,12 +471,14 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
if (qualityPopupMenuGroupId == menuItem.getGroupId()) {
if (selectedIndexStream == menuItem.getItemId()) return true;
setVideoStartPos(simpleExoPlayer.getCurrentPosition());
final int menuItemIndex = menuItem.getItemId();
if (selectedStreamIndex == menuItemIndex ||
availableStreams == null || availableStreams.size() <= menuItemIndex) return true;
selectedIndexStream = menuItem.getItemId();
if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying);
else qualityChanged = true;
final String newResolution = availableStreams.get(menuItemIndex).resolution;
setRecovery();
setPlaybackQuality(newResolution);
reload();
qualityTextView.setText(menuItem.getTitle());
return true;
@ -483,7 +487,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
float speed = PLAYBACK_SPEEDS[speedIndex];
setPlaybackSpeed(speed);
playbackSpeed.setText(formatSpeed(speed));
playbackSpeedTextView.setText(formatSpeed(speed));
}
return false;
@ -505,9 +509,10 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
isSomePopupMenuVisible = true;
showControls(300);
VideoStream videoStream = getSelectedVideoStream();
qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
wasPlaying = isPlaying();
final VideoStream videoStream = getSelectedVideoStream();
final String qualityText = MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution;
qualityTextView.setText(qualityText);
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
private void onPlaybackSpeedClicked() {
@ -533,7 +538,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK);
wasPlaying = isPlaying();
wasPlaying = simpleExoPlayer.getPlayWhenReady();
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
showControls(0);
@ -551,13 +556,25 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING);
if (!isProgressLoopRunning.get()) startProgressLoop();
if (!isProgressLoopRunning()) startProgressLoop();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
public int getVideoRendererIndex() {
if (simpleExoPlayer == null) return -1;
for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) {
if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_VIDEO) {
return t;
}
}
return -1;
}
public boolean isControlsVisible() {
return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE;
}
@ -652,6 +669,14 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// Getters and Setters
//////////////////////////////////////////////////////////////////////////*/
public void setPlaybackQuality(final String quality) {
this.playbackQuality = quality;
}
public String getPlaybackQuality() {
return playbackQuality;
}
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
return aspectRatioFrameLayout;
}
@ -665,39 +690,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
}
public VideoStream getSelectedVideoStream() {
return videoStreamsList.get(selectedIndexStream);
}
public Uri getSelectedStreamUri() {
return Uri.parse(getSelectedVideoStream().url);
}
public int getQualityPopupMenuGroupId() {
return qualityPopupMenuGroupId;
}
public int getSelectedStreamIndex() {
return selectedIndexStream;
}
public void setSelectedIndexStream(int selectedIndexStream) {
this.selectedIndexStream = selectedIndexStream;
}
public void setAudioStream(AudioStream audioStream) {
this.videoOnlyAudioStream = audioStream;
}
public AudioStream getAudioStream() {
return videoOnlyAudioStream;
}
public ArrayList<VideoStream> getVideoStreamsList() {
return videoStreamsList;
}
public void setVideoStreamsList(ArrayList<VideoStream> videoStreamsList) {
this.videoStreamsList = videoStreamsList;
return availableStreams.get(selectedStreamIndex);
}
public boolean isStartedFromNewPipe() {

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo;
public interface PlayerEventListener {
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters);
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
void onMetadataUpdate(StreamInfo info);
void onServiceStopped();
}

View File

@ -0,0 +1,188 @@
package org.schabi.newpipe.player.helper;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.audiofx.AudioEffect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener {
private static final String TAG = "AudioFocusReactor";
private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f;
private static final int FOCUS_GAIN_TYPE = AudioManager.AUDIOFOCUS_GAIN;
private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC;
private final SimpleExoPlayer player;
private final Context context;
private final AudioManager audioManager;
private final AudioFocusRequest request;
public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) {
this.player = player;
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
player.setAudioDebugListener(this);
if (shouldBuildFocusRequest()) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this)
.build();
} else {
request = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Audio Manager
//////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() {
if (shouldBuildFocusRequest()) {
audioManager.requestAudioFocus(request);
} else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
}
}
public void abandonAudioFocus() {
if (shouldBuildFocusRequest()) {
audioManager.abandonAudioFocusRequest(request);
} else {
audioManager.abandonAudioFocus(this);
}
}
public int getVolume() {
return audioManager.getStreamVolume(STREAM_TYPE);
}
public int getMaxVolume() {
return audioManager.getStreamMaxVolume(STREAM_TYPE);
}
public void setVolume(final int volume) {
audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
}
private boolean shouldBuildFocusRequest() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
/*//////////////////////////////////////////////////////////////////////////
// AudioFocus
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAudioFocusChange(int focusChange) {
Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]");
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
onAudioFocusGain();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
onAudioFocusLossCanDuck();
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
onAudioFocusLoss();
break;
}
}
private void onAudioFocusGain() {
Log.d(TAG, "onAudioFocusGain() called");
player.setVolume(DUCK_AUDIO_TO);
animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION);
if (PlayerHelper.isResumeAfterAudioFocusGain(context)) {
player.setPlayWhenReady(true);
}
}
private void onAudioFocusLoss() {
Log.d(TAG, "onAudioFocusLoss() called");
player.setPlayWhenReady(false);
}
private void onAudioFocusLossCanDuck() {
Log.d(TAG, "onAudioFocusLossCanDuck() called");
// Set the volume to 1/10 on ducking
animateAudio(player.getVolume(), DUCK_AUDIO_TO, DUCK_DURATION);
}
private void animateAudio(final float from, final float to, int duration) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setFloatValues(from, to);
valueAnimator.setDuration(duration);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
player.setVolume(from);
}
@Override
public void onAnimationCancel(Animator animation) {
player.setVolume(to);
}
@Override
public void onAnimationEnd(Animator animation) {
player.setVolume(to);
}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
player.setVolume(((float) animation.getAnimatedValue()));
}
});
valueAnimator.start();
}
/*//////////////////////////////////////////////////////////////////////////
// Audio Processing
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAudioSessionId(int i) {
if (!PlayerHelper.isUsingDSP(context)) return;
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, i);
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
context.sendBroadcast(intent);
}
@Override
public void onAudioEnabled(DecoderCounters decoderCounters) {}
@Override
public void onAudioDecoderInitialized(String s, long l, long l1) {}
@Override
public void onAudioInputFormatChanged(Format format) {}
@Override
public void onAudioTrackUnderrun(int i, long l, long l1) {}
@Override
public void onAudioDisabled(DecoderCounters decoderCounters) {}
}

View File

@ -0,0 +1,85 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import org.schabi.newpipe.Downloader;
import java.io.File;
public class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private final DefaultDataSourceFactory dataSourceFactory;
private final File cacheDir;
private final long maxFileSize;
// Creating cache on every instance may cause problems with multiple players when
// sources are not ExtractorMediaSource
// see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
// todo: make this a singleton?
private static SimpleCache cache;
public CacheFactory(@NonNull final Context context) {
this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context));
}
CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) {
super();
this.maxFileSize = maxFileSize;
final String userAgent = Downloader.USER_AGENT;
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored
cacheDir.mkdir();
}
if (cache == null) {
final LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize);
cache = new SimpleCache(cacheDir, evictor);
}
}
@Override
public DataSource createDataSource() {
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
final DefaultDataSource dataSource = dataSourceFactory.createDataSource();
final FileDataSource fileSource = new FileDataSource();
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
}
public void tryDeleteCacheFiles() {
if (!cacheDir.exists() || !cacheDir.isDirectory()) return;
try {
for (File file : cacheDir.listFiles()) {
final String filePath = file.getAbsolutePath();
final boolean deleteSuccessful = file.delete();
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
}
} catch (Exception ignored) {
Log.e(TAG, "Failed to delete file.", ignored);
}
}
}

View File

@ -0,0 +1,76 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
public class LoadController implements LoadControl {
public static final String TAG = "LoadController";
private final LoadControl internalLoadControl;
/*//////////////////////////////////////////////////////////////////////////
// Default Load Control
//////////////////////////////////////////////////////////////////////////*/
public LoadController(final Context context) {
this(PlayerHelper.getMinBufferMs(context),
PlayerHelper.getMaxBufferMs(context),
PlayerHelper.getBufferForPlaybackMs(context),
PlayerHelper.getBufferForPlaybackAfterRebufferMs(context));
}
public LoadController(final int minBufferMs,
final int maxBufferMs,
final long bufferForPlaybackMs,
final long bufferForPlaybackAfterRebufferMs) {
final DefaultAllocator allocator = new DefaultAllocator(true, 65536);
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
}
/*//////////////////////////////////////////////////////////////////////////
// Custom behaviours
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPrepared() {
internalLoadControl.onPrepared();
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) {
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
}
@Override
public void onStopped() {
internalLoadControl.onStopped();
}
@Override
public void onReleased() {
internalLoadControl.onReleased();
}
@Override
public Allocator getAllocator() {
return internalLoadControl.getAllocator();
}
@Override
public boolean shouldStartPlayback(long l, boolean b) {
return internalLoadControl.shouldStartPlayback(l, b);
}
@Override
public boolean shouldContinueLoading(long l) {
return internalLoadControl.shouldContinueLoading(l);
}
}

View File

@ -0,0 +1,44 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;
import static android.content.Context.POWER_SERVICE;
import static android.content.Context.WIFI_SERVICE;
public class LockManager {
private final String TAG = "LockManager@" + hashCode();
private final PowerManager powerManager;
private final WifiManager wifiManager;
private PowerManager.WakeLock wakeLock;
private WifiManager.WifiLock wifiLock;
public LockManager(final Context context) {
powerManager = ((PowerManager) context.getApplicationContext().getSystemService(POWER_SERVICE));
wifiManager = ((WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE));
}
public void acquireWifiAndCpu() {
Log.d(TAG, "acquireWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return;
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
if (wakeLock != null) wakeLock.acquire();
if (wifiLock != null) wifiLock.acquire();
}
public void releaseWifiAndCpu() {
Log.d(TAG, "releaseWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
if (wifiLock != null && wifiLock.isHeld()) wifiLock.release();
wakeLock = null;
wifiLock = null;
}
}

View File

@ -0,0 +1,106 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import org.schabi.newpipe.R;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Formatter;
import java.util.Locale;
public class PlayerHelper {
private PlayerHelper() {}
private static final StringBuilder stringBuilder = new StringBuilder();
private static final Formatter stringFormatter = new Formatter(stringBuilder, Locale.getDefault());
private static final NumberFormat speedFormatter = new DecimalFormat("0.##x");
private static final NumberFormat pitchFormatter = new DecimalFormat("##%");
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
public static String getTimeString(int milliSeconds) {
long seconds = (milliSeconds % 60000L) / 1000L;
long minutes = (milliSeconds % 3600000L) / 60000L;
long hours = (milliSeconds % 86400000L) / 3600000L;
long days = (milliSeconds % (86400000L * 7L)) / 86400000L;
stringBuilder.setLength(0);
return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
: hours > 0 ? stringFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: stringFormatter.format("%02d:%02d", minutes, seconds).toString();
}
public static String formatSpeed(float speed) {
return speedFormatter.format(speed);
}
public static String formatPitch(float pitch) {
return pitchFormatter.format(pitch);
}
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return isResumeAfterAudioFocusGain(context, false);
}
public static boolean isPlayerGestureEnabled(@NonNull final Context context) {
return isPlayerGestureEnabled(context, true);
}
public static boolean isUsingOldPlayer(@NonNull final Context context) {
return isUsingOldPlayer(context, false);
}
public static long getPreferredCacheSize(@NonNull final Context context) {
return 64 * 1024 * 1024L;
}
public static long getPreferredFileSize(@NonNull final Context context) {
return 512 * 1024L;
}
public static int getMinBufferMs(@NonNull final Context context) {
return 15000;
}
public static int getMaxBufferMs(@NonNull final Context context) {
return 30000;
}
public static long getBufferForPlaybackMs(@NonNull final Context context) {
return 2500L;
}
public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) {
return 5000L;
}
public static boolean isUsingDSP(@NonNull final Context context) {
return true;
}
////////////////////////////////////////////////////////////////////////////
// Private helpers
////////////////////////////////////////////////////////////////////////////
@NonNull
private static SharedPreferences getPreferences(@NonNull final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
}
private static boolean isPlayerGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.player_gesture_controls_key), b);
}
private static boolean isUsingOldPlayer(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b);
}
}

View File

@ -0,0 +1,238 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
/**
* DeferredMediaSource is specifically designed to allow external control over when
* the source metadata are loaded while being compatible with ExoPlayer's playlists.
*
* This media source follows the structure of how NewPipeExtractor's
* {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into
* {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete,
* this media source behaves identically as any other native media sources.
* */
public final class DeferredMediaSource implements MediaSource {
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
/**
* This state indicates the {@link DeferredMediaSource} has just been initialized or reset.
* The source must be prepared and loaded again before playback.
* */
public final static int STATE_INIT = 0;
/**
* This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
* */
public final static int STATE_PREPARED = 1;
/**
* This state indicates the {@link DeferredMediaSource} has been loaded without errors and
* is ready for playback.
* */
public final static int STATE_LOADED = 2;
public interface Callback {
/**
* Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
* from a given StreamInfo.
* */
MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
}
private PlayQueueItem stream;
private Callback callback;
private int state;
private MediaSource mediaSource;
/* Custom internal objects */
private Disposable loader;
private ExoPlayer exoPlayer;
private Listener listener;
private Throwable error;
public DeferredMediaSource(@NonNull final PlayQueueItem stream,
@NonNull final Callback callback) {
this.stream = stream;
this.callback = callback;
this.state = STATE_INIT;
}
/**
* Returns the current state of the {@link DeferredMediaSource}.
*
* @see DeferredMediaSource#STATE_INIT
* @see DeferredMediaSource#STATE_PREPARED
* @see DeferredMediaSource#STATE_LOADED
* */
public int state() {
return state;
}
/**
* Parameters are kept in the class for delayed preparation.
* */
@Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer;
this.listener = listener;
this.state = STATE_PREPARED;
}
/**
* Externally controlled loading. This method fully prepares the source to be used
* like any other native {@link com.google.android.exoplayer2.source.MediaSource}.
*
* Ideally, this should be called after this source has entered PREPARED state and
* called once only.
*
* If loading fails here, an error will be propagated out and result in an
* {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException},
* which is delegated to the player.
* */
public synchronized void load() {
if (stream == null) {
Log.e(TAG, "Stream Info missing, media source loading terminated.");
return;
}
if (state != STATE_PREPARED || loader != null) return;
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() {
@Override
public MediaSource apply(StreamInfo streamInfo) throws Exception {
return onStreamInfoReceived(stream, streamInfo);
}
};
final Consumer<MediaSource> onSuccess = new Consumer<MediaSource>() {
@Override
public void accept(MediaSource mediaSource) throws Exception {
onMediaSourceReceived(mediaSource);
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
onStreamInfoError(throwable);
}
};
loader = stream.getStream()
.observeOn(Schedulers.io())
.map(onReceive)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError);
}
private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
@NonNull final StreamInfo info) throws Exception {
if (callback == null) {
throw new Exception("No available callback for resolving stream info.");
}
final MediaSource mediaSource = callback.sourceOf(item, info);
if (mediaSource == null) {
throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() +
", audio count: " + info.audio_streams.size() +
", video count: " + info.video_only_streams.size() + info.video_streams.size());
}
return mediaSource;
}
private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception {
if (exoPlayer == null || listener == null || mediaSource == null) {
throw new Exception("MediaSource loading failed. URL: " + stream.getUrl());
}
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
state = STATE_LOADED;
this.mediaSource = mediaSource;
this.mediaSource.prepareSource(exoPlayer, false, listener);
}
private void onStreamInfoError(final Throwable throwable) {
Log.e(TAG, "Loading error:", throwable);
error = throwable;
state = STATE_LOADED;
}
/**
* Delegate all errors to the player after {@link #load() load} is complete.
*
* Specifically, this method is called after an exception has occurred during loading or
* {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}.
* */
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (error != null) {
throw new IOException(error);
}
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
return mediaSource.createPeriod(mediaPeriodId, allocator);
}
/**
* Releases the media period (buffers).
*
* This may be called after {@link #releaseSource releaseSource}.
* */
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
mediaSource.releasePeriod(mediaPeriod);
}
/**
* Cleans up all internal custom objects creating during loading.
*
* This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource}
* is released or when the player is stopped.
*
* This method should not release or set null the resources passed in through the constructor.
* This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}.
* */
@Override
public void releaseSource() {
if (mediaSource != null) {
mediaSource.releaseSource();
}
if (loader != null) {
loader.dispose();
}
/* Do not set mediaSource as null here as it may be called through releasePeriod */
loader = null;
exoPlayer = null;
listener = null;
error = null;
state = STATE_INIT;
}
}

View File

@ -0,0 +1,356 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.subjects.PublishSubject;
public class MediaSourceManager {
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
// One-side rolling window size for default loading
// Effectively loads windowSize * 2 + 1 streams, must be greater than 0
private final int windowSize;
private final PlaybackListener playbackListener;
private final PlayQueue playQueue;
// Process only the last load order when receiving a stream of load orders (lessens I/O)
// The higher it is, the less loading occurs during rapid noncritical timeline changes
// Not recommended to go below 100ms
private final long loadDebounceMillis;
private final PublishSubject<Long> loadSignal;
private final Disposable debouncedLoader;
private final DeferredMediaSource.Callback sourceBuilder;
private DynamicConcatenatingMediaSource sources;
private Subscription playQueueReactor;
private SerialDisposable syncReactor;
private boolean isBlocked;
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, 1, 1000L);
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final int windowSize,
final long loadDebounceMillis) {
if (windowSize <= 0) {
throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0");
}
this.playbackListener = listener;
this.playQueue = playQueue;
this.windowSize = windowSize;
this.loadDebounceMillis = loadDebounceMillis;
this.syncReactor = new SerialDisposable();
this.loadSignal = PublishSubject.create();
this.debouncedLoader = getDebouncedLoader();
this.sourceBuilder = getSourceBuilder();
this.sources = new DynamicConcatenatingMediaSource();
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getReactor());
}
/*//////////////////////////////////////////////////////////////////////////
// DeferredMediaSource listener
//////////////////////////////////////////////////////////////////////////*/
private DeferredMediaSource.Callback getSourceBuilder() {
return new DeferredMediaSource.Callback() {
@Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
return playbackListener.sourceOf(item, info);
}
};
}
/*//////////////////////////////////////////////////////////////////////////
// Exposed Methods
//////////////////////////////////////////////////////////////////////////*/
/**
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() {
if (loadSignal != null) loadSignal.onComplete();
if (debouncedLoader != null) debouncedLoader.dispose();
if (playQueueReactor != null) playQueueReactor.cancel();
if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource();
playQueueReactor = null;
syncReactor = null;
sources = null;
}
/**
* 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() {
loadSignal.onNext(System.currentTimeMillis());
}
/**
* 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() {
tryBlock();
populateSources();
}
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
private Subscriber<PlayQueueEvent> getReactor() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Subscription d) {
if (playQueueReactor != null) playQueueReactor.cancel();
playQueueReactor = d;
playQueueReactor.request(1);
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
}
@Override
public void onError(@NonNull Throwable e) {}
@Override
public void onComplete() {}
};
}
private void onPlayQueueChanged(final PlayQueueEvent event) {
if (playQueue.isEmpty()) {
playbackListener.shutdown();
return;
}
// why no pattern matching in Java =(
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
reset();
break;
case APPEND:
populateSources();
break;
case SELECT:
sync();
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex());
// Sync only when the currently playing is removed
if (removeEvent.getQueueIndex() == removeEvent.getRemoveIndex()) sync();
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case RECOVERY:
default:
break;
}
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
case APPEND:
loadInternal(); // low frequency, critical events
break;
case REMOVE:
case SELECT:
case MOVE:
case RECOVERY:
default:
load(); // high frequency or noncritical events
break;
}
if (!isPlayQueueReady()) {
tryBlock();
playQueue.fetch();
}
if (playQueueReactor != null) playQueueReactor.request(1);
}
/*//////////////////////////////////////////////////////////////////////////
// Internal Helpers
//////////////////////////////////////////////////////////////////////////*/
private boolean isPlayQueueReady() {
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > windowSize;
}
private boolean tryBlock() {
if (!isBlocked) {
playbackListener.block();
resetSources();
isBlocked = true;
return true;
}
return false;
}
private boolean tryUnblock() {
if (isPlayQueueReady() && isBlocked && sources != null) {
isBlocked = false;
playbackListener.unblock(sources);
return true;
}
return false;
}
private void sync() {
final PlayQueueItem currentItem = playQueue.getItem();
if (currentItem == null) return;
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
@Override
public void accept(StreamInfo streamInfo) throws Exception {
playbackListener.sync(currentItem, streamInfo);
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "Sync error:", throwable);
playbackListener.sync(currentItem,null);
}
};
syncReactor.set(currentItem.getStream().subscribe(syncPlayback, onError));
}
private void loadInternal() {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
loadItem(currentItem);
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - windowSize);
final int rightLimit = currentIndex + windowSize + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
for (final PlayQueueItem item: items) loadItem(item);
}
private void loadItem(@Nullable final PlayQueueItem item) {
if (item == null) return;
final int index = playQueue.indexOf(item);
if (index > sources.getSize() - 1) return;
final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
if (tryUnblock()) sync();
}
private void resetSources() {
if (this.sources != null) this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource();
}
private void populateSources() {
if (sources == null) return;
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder));
}
}
private Disposable getDebouncedLoader() {
return loadSignal
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long timestamp) throws Exception {
loadInternal();
}
});
}
/*//////////////////////////////////////////////////////////////////////////
// Media Source List Manipulation
//////////////////////////////////////////////////////////////////////////*/
/**
* Inserts a source into {@link DynamicConcatenatingMediaSource} with position
* in respect to the play queue.
*
* If the play queue index already exists, then the insert is ignored.
* */
private void insert(final int queueIndex, final DeferredMediaSource source) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex < sources.getSize()) return;
sources.addMediaSource(queueIndex, source);
}
/**
* Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index.
*
* If the play queue index does not exist, the removal is ignored.
* */
private void remove(final int queueIndex) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex > sources.getSize()) return;
sources.removeMediaSource(queueIndex);
}
private void move(final int source, final int target) {
if (sources == null) return;
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
}

View File

@ -0,0 +1,56 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List;
public interface PlaybackListener {
/**
* 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
* is now invalid.
*
* May be called at any time.
* */
void block();
/**
* Called when the stream at the current queue index is ready.
* Signals to the listener to resume the player by preparing a new source.
*
* May be called only when the player is blocked.
* */
void unblock(final MediaSource mediaSource);
/**
* Called when the queue index is refreshed.
* Signals to the listener to synchronize the player's window to the manager's
* window.
*
* May be called only after unblock is called.
* */
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
/**
* Requests the listener to resolve a stream info into a media source
* according to the listener's implementation (background, popup or main video player).
*
* May be called at any time.
* */
MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
/**
* Called when the play queue can no longer to played or used.
* Currently, this means the play queue is empty and complete.
* Signals to the listener that it should shutdown.
*
* May be called at any time.
* */
void shutdown();
}

View File

@ -0,0 +1,104 @@
package org.schabi.newpipe.playlist;
import android.util.Log;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public final class ExternalPlayQueue extends PlayQueue {
private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode());
private boolean isComplete;
private int serviceId;
private String baseUrl;
private String nextUrl;
private transient Disposable fetchReactor;
public ExternalPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final int index) {
super(index, extractPlaylistItems(streams));
this.baseUrl = url;
this.nextUrl = nextPageUrl;
this.serviceId = serviceId;
this.isComplete = nextPageUrl == null || nextPageUrl.isEmpty();
}
@Override
public boolean isComplete() {
return isComplete;
}
@Override
public void fetch() {
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistObserver());
}
private SingleObserver<ListExtractor.NextItemsResult> getPlaylistObserver() {
return new SingleObserver<ListExtractor.NextItemsResult>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || (fetchReactor != null && !fetchReactor.isDisposed())) {
d.dispose();
} else {
fetchReactor = d;
}
}
@Override
public void onSuccess(@NonNull ListExtractor.NextItemsResult result) {
if (!result.hasMoreStreams()) isComplete = true;
nextUrl = result.nextItemsUrl;
append(extractPlaylistItems(result.nextItemsList));
fetchReactor.dispose();
fetchReactor = null;
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(TAG, "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
append(); // Notify change
}
};
}
@Override
public void dispose() {
super.dispose();
if (fetchReactor != null) fetchReactor.dispose();
}
private static List<PlayQueueItem> extractPlaylistItems(final List<InfoItem> infos) {
List<PlayQueueItem> result = new ArrayList<>();
for (final InfoItem stream : infos) {
if (stream instanceof StreamInfoItem) {
result.add(new PlayQueueItem((StreamInfoItem) stream));
}
}
return result;
}
}

View File

@ -0,0 +1,427 @@
package org.schabi.newpipe.playlist;
import android.support.annotation.NonNull;
import android.util.Log;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.InitEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RecoveryEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.playlist.events.SelectEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.BehaviorSubject;
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing.
*
* This class contains basic manipulation of a playlist while also functions as a
* message bus, providing all listeners with new updates to the play queue.
*
* This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized.
* */
public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
public static final boolean DEBUG = true;
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>();
streams.addAll(startWith);
queueIndex = new AtomicInteger(index);
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist actions
//////////////////////////////////////////////////////////////////////////*/
/**
* Initializes the play queue message buses.
*
* Also starts a self reporter for logging if debug mode is enabled.
* */
public void init() {
eventBroadcast = BehaviorSubject.create();
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread())
.startWith(new InitEvent());
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
}
/**
* Dispose the play queue by stopping all message buses.
* */
public void dispose() {
if (eventBroadcast != null) eventBroadcast.onComplete();
if (reportingReactor != null) reportingReactor.cancel();
broadcastReceiver = null;
reportingReactor = null;
}
/**
* Checks if the queue is complete.
*
* A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete.
* */
public abstract boolean isComplete();
/**
* Load partial queue in the background, does nothing if the queue is complete.
* */
public abstract void fetch();
/*//////////////////////////////////////////////////////////////////////////
// Readonly ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the current index that should be played.
* */
public int getIndex() {
return queueIndex.get();
}
/**
* Returns the current item that should be played.
* */
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* Returns the item at the given index.
* May throw {@link IndexOutOfBoundsException}.
* */
public PlayQueueItem getItem(int index) {
if (index >= streams.size() || streams.get(index) == null) return null;
return streams.get(index);
}
/**
* Returns the index of the given item using referential equality.
* May be null despite play queue contains identical item.
* */
public int indexOf(final PlayQueueItem item) {
// referential equality, can't think of a better way to do this
// todo: better than this
return streams.indexOf(item);
}
/**
* Returns the current size of play queue.
* */
public int size() {
return streams.size();
}
/**
* Checks if the play queue is empty.
* */
public boolean isEmpty() {
return streams.isEmpty();
}
/**
* Determines if the current play queue is shuffled.
* */
public boolean isShuffled() {
return backup != null;
}
/**
* Returns an immutable view of the play queue.
* */
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
* */
@NonNull
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/*//////////////////////////////////////////////////////////////////////////
// Write ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Changes the current playing index to a new index.
*
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* */
public synchronized void setIndex(final int index) {
final int oldIndex = getIndex();
int newIndex = index;
if (index < 0) newIndex = 0;
if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
queueIndex.set(newIndex);
broadcast(new SelectEvent(oldIndex, newIndex));
}
/**
* Changes the current playing index by an offset amount.
*
* Will emit a {@link SelectEvent} if offset is non-zero.
* */
public synchronized void offsetIndex(final int offset) {
setIndex(getIndex() + offset);
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* @see #append(List items)
* */
public synchronized void append(final PlayQueueItem... items) {
append(Arrays.asList(items));
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* If the play queue is shuffled, then append the items to the backup queue as is and
* append the shuffle items to the play queue.
*
* Will emit a {@link AppendEvent} on any given context.
* */
public synchronized void append(final List<PlayQueueItem> items) {
List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
backup.addAll(itemList);
Collections.shuffle(itemList);
}
streams.addAll(itemList);
broadcast(new AppendEvent(itemList.size()));
}
/**
* Removes the item at the given index from the play queue.
*
* The current playing index will decrement if it is greater than the index being removed.
* On cases where the current playing index exceeds the playlist range, it is set to 0.
*
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* */
public synchronized void remove(final int index) {
if (index >= streams.size() || index < 0) return;
removeInternal(index);
broadcast(new RemoveEvent(index, getIndex()));
}
/**
* Report an exception for the item at the current index in order and the course of action:
* if the error can be skipped or the current item should be removed.
*
* This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions.
* */
public synchronized void error(final boolean skippable) {
final int index = getIndex();
if (skippable) {
queueIndex.incrementAndGet();
} else {
removeInternal(index);
}
broadcast(new ErrorEvent(index, getIndex(), skippable));
}
private synchronized void removeInternal(final int removeIndex) {
final int currentIndex = queueIndex.get();
final int size = size();
if (currentIndex > removeIndex) {
queueIndex.decrementAndGet();
} else if (currentIndex >= size) {
queueIndex.set(currentIndex % (size - 1));
} else if (currentIndex == removeIndex && currentIndex == size - 1){
queueIndex.set(removeIndex - 1);
}
if (backup != null) {
final int backupIndex = backup.indexOf(getItem(removeIndex));
backup.remove(backupIndex);
}
streams.remove(removeIndex);
}
/**
* Moves a queue item at the source index to the target index.
*
* If the item being moved is the currently playing, then the current playing index is set
* to that of the target.
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the
* current playing index, then the current playing index is decremented.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
* */
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= streams.size() || target >= streams.size()) return;
final int current = getIndex();
if (source == current) {
queueIndex.set(target);
} else if (source < current && target >= current) {
queueIndex.decrementAndGet();
} else if (source > current && target <= current) {
queueIndex.incrementAndGet();
}
streams.add(target, streams.remove(source));
broadcast(new MoveEvent(source, target));
}
/**
* Sets the recovery record of the item at the index.
*
* Broadcasts a recovery event.
* */
public synchronized void setRecovery(final int index, final long position) {
if (index < 0 || index >= streams.size()) return;
streams.get(index).setRecoveryPosition(position);
broadcast(new RecoveryEvent(index, position));
}
/**
* Revoke the recovery record of the item at the index.
*
* Broadcasts a recovery event.
* */
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
}
/**
* Shuffles the current play queue.
*
* This method first backs up the existing play queue and item being played.
* Then a newly shuffled play queue will be generated along with currently
* playing item placed at the beginning of the queue.
*
* Will emit a {@link ReorderEvent} in any context.
* */
public synchronized void shuffle() {
if (backup == null) {
backup = new ArrayList<>(streams);
}
final PlayQueueItem current = getItem();
Collections.shuffle(streams);
final int newIndex = streams.indexOf(current);
if (newIndex != -1) {
streams.add(0, streams.remove(newIndex));
}
queueIndex.set(0);
broadcast(new ReorderEvent());
}
/**
* Unshuffles the current play queue if a backup play queue exists.
*
* This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0.
*
* Will emit a {@link ReorderEvent} if a backup exists.
* */
public synchronized void unshuffle() {
if (backup == null) return;
final PlayQueueItem current = getItem();
streams.clear();
streams = backup;
backup = null;
final int newIndex = streams.indexOf(current);
if (newIndex != -1) {
queueIndex.set(newIndex);
} else {
queueIndex.set(0);
}
broadcast(new ReorderEvent());
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/
private void broadcast(final PlayQueueEvent event) {
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}
}
private Subscriber<PlayQueueEvent> getSelfReporter() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(Subscription s) {
if (reportingReactor != null) reportingReactor.cancel();
reportingReactor = s;
reportingReactor.request(1);
}
@Override
public void onNext(PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + ".");
reportingReactor.request(1);
}
@Override
public void onError(Throwable t) {
Log.e(TAG, "Received broadcast error", t);
}
@Override
public void onComplete() {
Log.d(TAG, "Broadcast is shutting down.");
}
};
}
}

View File

@ -0,0 +1,204 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.SelectEvent;
import java.util.List;
import io.reactivex.Observer;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoListAdapter.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = PlayQueueAdapter.class.toString();
private static final int ITEM_VIEW_TYPE_ID = 0;
private static final int FOOTER_VIEW_TYPE_ID = 1;
private final PlayQueueItemBuilder playQueueItemBuilder;
private final PlayQueue playQueue;
private boolean showFooter = false;
private View footer = null;
private Disposable playQueueReactor;
public class HFHolder extends RecyclerView.ViewHolder {
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public PlayQueueAdapter(final Context context, final PlayQueue playQueue) {
this.playQueueItemBuilder = new PlayQueueItemBuilder(context);
this.playQueue = playQueue;
startReactor();
}
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
playQueueItemBuilder.setOnSelectedListener(listener);
}
private void startReactor() {
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (playQueueReactor != null) playQueueReactor.dispose();
playQueueReactor = d;
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
}
@Override
public void onError(@NonNull Throwable e) {}
@Override
public void onComplete() {
dispose();
}
};
playQueue.getBroadcastReceiver().toObservable().subscribe(observer);
}
private void onPlayQueueChanged(final PlayQueueEvent message) {
switch (message.type()) {
case RECOVERY:
// Do nothing.
break;
case SELECT:
final SelectEvent selectEvent = (SelectEvent) message;
notifyItemChanged(selectEvent.getOldIndex());
notifyItemChanged(selectEvent.getNewIndex());
break;
case APPEND:
final AppendEvent appendEvent = (AppendEvent) message;
notifyItemRangeInserted(playQueue.size(), appendEvent.getAmount());
break;
case ERROR:
final ErrorEvent errorEvent = (ErrorEvent) message;
if (!errorEvent.isSkippable()) {
notifyItemRemoved(errorEvent.getErrorIndex());
}
notifyItemChanged(errorEvent.getErrorIndex());
notifyItemChanged(errorEvent.getQueueIndex());
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) message;
notifyItemRemoved(removeEvent.getRemoveIndex());
notifyItemChanged(removeEvent.getQueueIndex());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) message;
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case INIT:
case REORDER:
default:
notifyDataSetChanged();
break;
}
}
public void dispose() {
if (playQueueReactor != null) playQueueReactor.dispose();
playQueueReactor = null;
}
public void setFooter(View footer) {
this.footer = footer;
notifyItemChanged(playQueue.size());
}
public void showFooter(final boolean show) {
showFooter = show;
notifyItemChanged(playQueue.size());
}
public List<PlayQueueItem> getItems() {
return playQueue.getStreams();
}
@Override
public int getItemCount() {
int count = playQueue.getStreams().size();
if(footer != null && showFooter) count++;
return count;
}
@Override
public int getItemViewType(int position) {
if(footer != null && position == playQueue.getStreams().size() && showFooter) {
return FOOTER_VIEW_TYPE_ID;
}
return ITEM_VIEW_TYPE_ID;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
case FOOTER_VIEW_TYPE_ID:
return new HFHolder(footer);
case ITEM_VIEW_TYPE_ID:
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
default:
Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) {
final PlayQueueItemHolder itemHolder = (PlayQueueItemHolder) holder;
// Build the list item
playQueueItemBuilder.buildStreamInfoItem(itemHolder, playQueue.getStreams().get(position));
// Check if the current item should be selected/highlighted
final boolean isSelected = playQueue.getIndex() == position;
itemHolder.itemSelected.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
itemHolder.itemView.setSelected(isSelected);
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
}

View File

@ -0,0 +1,117 @@
package org.schabi.newpipe.playlist;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable {
final public static long RECOVERY_UNSET = Long.MIN_VALUE;
final private String title;
final private String url;
final private int serviceId;
final private long duration;
final private String thumbnailUrl;
final private String uploader;
private long recoveryPosition;
private Throwable error;
private transient Single<StreamInfo> stream;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.name, info.url, info.service_id, info.duration, info.thumbnail_url, info.uploader_name);
this.stream = Single.just(info);
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.name, item.url, item.service_id, item.duration, item.thumbnail_url, item.uploader_name);
}
private PlayQueueItem(final String name, final String url, final int serviceId,
final long duration, final String thumbnailUrl, final String uploader) {
this.title = name;
this.url = url;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.recoveryPosition = RECOVERY_UNSET;
}
@NonNull
public String getTitle() {
return title;
}
@NonNull
public String getUrl() {
return url;
}
public int getServiceId() {
return serviceId;
}
public long getDuration() {
return duration;
}
@NonNull
public String getThumbnailUrl() {
return thumbnailUrl;
}
@NonNull
public String getUploader() {
return uploader;
}
public long getRecoveryPosition() {
return recoveryPosition;
}
@Nullable
public Throwable getError() {
return error;
}
@NonNull
public Single<StreamInfo> getStream() {
return stream == null ? stream = getInfo() : stream;
}
@NonNull
private Single<StreamInfo> getInfo() {
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
error = throwable;
}
};
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(onError);
}
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out
////////////////////////////////////////////////////////////////////////////
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition;
}
}

View File

@ -0,0 +1,112 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
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.util.Localization;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
private final int thumbnailWidthPx;
private final int thumbnailHeightPx;
private final DisplayImageOptions imageOptions;
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
private OnSelectedListener onItemClickListener;
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) {
this.onItemClickListener = listener;
}
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.selected(item, view);
}
}
});
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.held(item, view);
return true;
}
return false;
}
});
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
}
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.performClick();
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
onItemClickListener.onStartDrag(holder);
}
return false;
}
};
}
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
final BitmapProcessor bitmapProcessor = new BitmapProcessor() {
@Override
public Bitmap process(Bitmap 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)
.build();
}
}

View File

@ -0,0 +1,49 @@
package org.schabi.newpipe.playlist;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
/**
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
public final ImageView itemSelected, itemThumbnailView, itemHandle;
public final View itemRoot;
public PlayQueueItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemSelected = v.findViewById(R.id.itemSelected);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
itemHandle = v.findViewById(R.id.itemHandle);
}
}

View File

@ -0,0 +1,19 @@
package org.schabi.newpipe.playlist;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.Collections;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfo info) {
super(0, Collections.singletonList(new PlayQueueItem(info)));
}
@Override
public boolean isComplete() {
return true;
}
@Override
public void fetch() {}
}

View File

@ -0,0 +1,19 @@
package org.schabi.newpipe.playlist.events;
public class AppendEvent implements PlayQueueEvent {
final private int amount;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.APPEND;
}
public AppendEvent(final int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
}

View File

@ -0,0 +1,31 @@
package org.schabi.newpipe.playlist.events;
public class ErrorEvent implements PlayQueueEvent {
final private int errorIndex;
final private int queueIndex;
final private boolean skippable;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.ERROR;
}
public ErrorEvent(final int errorIndex, final int queueIndex, final boolean skippable) {
this.errorIndex = errorIndex;
this.queueIndex = queueIndex;
this.skippable = skippable;
}
public int getErrorIndex() {
return errorIndex;
}
public int getQueueIndex() {
return queueIndex;
}
public boolean isSkippable() {
return skippable;
}
}

View File

@ -0,0 +1,8 @@
package org.schabi.newpipe.playlist.events;
public class InitEvent implements PlayQueueEvent {
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.INIT;
}
}

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.playlist.events;
public class MoveEvent implements PlayQueueEvent {
final private int fromIndex;
final private int toIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.MOVE;
}
public MoveEvent(final int oldIndex, final int newIndex) {
this.fromIndex = oldIndex;
this.toIndex = newIndex;
}
public int getFromIndex() {
return fromIndex;
}
public int getToIndex() {
return toIndex;
}
}

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.playlist.events;
import java.io.Serializable;
public interface PlayQueueEvent extends Serializable {
PlayQueueEventType type();
}

View File

@ -0,0 +1,27 @@
package org.schabi.newpipe.playlist.events;
public enum PlayQueueEventType {
INIT,
// sent when the index is changed
SELECT,
// sent when more streams are added to the play queue
APPEND,
// sent when a pending stream is removed from the play queue
REMOVE,
// sent when two streams swap place in the play queue
MOVE,
// sent when queue is shuffled
REORDER,
// sent when recovery record is set on a stream
RECOVERY,
// sent when the item at index has caused an exception
ERROR
}

View File

@ -0,0 +1,25 @@
package org.schabi.newpipe.playlist.events;
public class RecoveryEvent implements PlayQueueEvent {
final private int index;
final private long position;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.RECOVERY;
}
public RecoveryEvent(final int index, final long position) {
this.index = index;
this.position = position;
}
public int getIndex() {
return index;
}
public long getPosition() {
return position;
}
}

View File

@ -0,0 +1,25 @@
package org.schabi.newpipe.playlist.events;
public class RemoveEvent implements PlayQueueEvent {
final private int removeIndex;
final private int queueIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REMOVE;
}
public RemoveEvent(final int removeIndex, final int queueIndex) {
this.removeIndex = removeIndex;
this.queueIndex = queueIndex;
}
public int getQueueIndex() {
return queueIndex;
}
public int getRemoveIndex() {
return removeIndex;
}
}

View File

@ -0,0 +1,12 @@
package org.schabi.newpipe.playlist.events;
public class ReorderEvent implements PlayQueueEvent {
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REORDER;
}
public ReorderEvent() {
}
}

View File

@ -0,0 +1,25 @@
package org.schabi.newpipe.playlist.events;
public class SelectEvent implements PlayQueueEvent {
final private int oldIndex;
final private int newIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.SELECT;
}
public SelectEvent(final int oldIndex, final int newIndex) {
this.oldIndex = oldIndex;
this.newIndex = newIndex;
}
public int getOldIndex() {
return oldIndex;
}
public int getNewIndex() {
return newIndex;
}
}

View File

@ -56,6 +56,13 @@ public final class ListHelper {
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value));
return getDefaultResolutionIndex(context, videoStreams, defaultResolution);
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getDefaultResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}
@ -67,6 +74,13 @@ public final class ListHelper {
if (defaultPreferences == null) return 0;
String defaultResolution = defaultPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value));
return getPopupDefaultResolutionIndex(context, videoStreams, defaultResolution);
}
/**
* @see #getDefaultResolutionIndex(String, String, MediaFormat, List)
*/
public static int getPopupDefaultResolutionIndex(Context context, List<VideoStream> videoStreams, String defaultResolution) {
return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams);
}

View File

@ -6,6 +6,7 @@ import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.support.annotation.PluralsRes;
import android.support.annotation.StringRes;
import android.text.TextUtils;
import org.schabi.newpipe.R;

View File

@ -1,11 +1,14 @@
package org.schabi.newpipe.util;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import com.nostra13.universalimageloader.core.ImageLoader;
@ -17,8 +20,6 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
@ -27,13 +28,11 @@ import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.history.HistoryActivity;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@SuppressWarnings({"unused", "WeakerAccess"})
public class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@ -41,46 +40,41 @@ public class NavigationHelper {
/*//////////////////////////////////////////////////////////////////////////
// Players
//////////////////////////////////////////////////////////////////////////*/
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue,
final String quality) {
Intent intent = new Intent(context, targetClazz)
.putExtra(VideoPlayer.PLAY_QUEUE, playQueue);
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) {
Intent mIntent = new Intent(context, targetClazz)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex)
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)))
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, ListHelper.getHighestQualityAudio(info.audio_streams));
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
return intent;
}
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, VideoPlayer instance) {
return new Intent(context, targetClazz)
.putExtra(BasePlayer.VIDEO_TITLE, instance.getVideoTitle())
.putExtra(BasePlayer.VIDEO_URL, instance.getVideoUrl())
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, instance.getVideoThumbnailUrl())
.putExtra(BasePlayer.CHANNEL_NAME, instance.getUploaderName())
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex())
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList())
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream())
.putExtra(BasePlayer.START_POSITION, instance.getPlayer().getCurrentPosition())
.putExtra(BasePlayer.PLAYBACK_SPEED, instance.getPlaybackSpeed());
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue) {
return getPlayerIntent(context, targetClazz, playQueue, null);
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info) {
return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(ListHelper.getDefaultAudioFormat(context, info.audio_streams)));
public static Intent getPlayerEnqueueIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue) {
return getPlayerIntent(context, targetClazz, playQueue)
.putExtra(BasePlayer.APPEND_ONLY, true);
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info, AudioStream audioStream) {
Intent mIntent = new Intent(context, BackgroundPlayer.class)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(BackgroundPlayer.AUDIO_STREAM, audioStream);
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final String playbackQuality) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch);
}
/*//////////////////////////////////////////////////////////////////////////
@ -303,4 +297,55 @@ public class NavigationHelper {
}
return null;
}
private static Uri openMarketUrl(String packageName) {
return Uri.parse("market://details")
.buildUpon()
.appendQueryParameter("id", packageName)
.build();
}
private static Uri getGooglePlayUrl(String packageName) {
return Uri.parse("https://play.google.com/store/apps/details")
.buildUpon()
.appendQueryParameter("id", packageName)
.build();
}
private static void installApp(Context context, String packageName) {
try {
// Try market:// scheme
context.startActivity(new Intent(Intent.ACTION_VIEW, openMarketUrl(packageName)));
} catch (ActivityNotFoundException e) {
// Fall back to google play URL (don't worry F-Droid can handle it :)
context.startActivity(new Intent(Intent.ACTION_VIEW, getGooglePlayUrl(packageName)));
}
}
/**
* Start an activity to install Kore
* @param context the context
*/
public static void installKore(Context context) {
installApp(context, context.getString(R.string.kore_package));
}
/**
* Start Kore app to show a video on Kodi
*
* For a list of supported urls see the
* <a href="https://github.com/xbmc/Kore/blob/master/app/src/main/AndroidManifest.xml">
* Kore source code
* </a>.
*
* @param context the context to use
* @param videoURL the url to the video
*/
public static void playWithKore(Context context, Uri videoURL) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(context.getString(R.string.kore_package));
intent.setData(videoURL);
context.startActivity(intent);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@color/selected_background_color"/>
<item android:drawable="@color/transparent_background_color"/>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@color/selected_background_color"/>
<item android:drawable="@color/transparent_background_color"/>
</selector>

View File

@ -0,0 +1,300 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_weight="1"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="@string/app_name"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/play_queue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/appbar"
android:layout_above="@id/progress_bar"
android:layout_toLeftOf="@+id/control_pane"
android:layout_toStartOf="@+id/control_pane"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
<RelativeLayout
android:id="@+id/control_pane"
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_above="@id/progress_bar"
android:layout_below="@id/appbar">
<LinearLayout
android:id="@+id/metadata"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/playback_controls_top"
android:orientation="vertical"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry">
<TextView
android:id="@+id/song_name"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="4"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta."
/>
<TextView
android:id="@+id/artist_name"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textSize="12sp"
tools:text="Duis posuere arcu condimentum lobortis mattis." />
</LinearLayout>
<TextView
android:id="@+id/seek_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#c0000000"
android:paddingBottom="5dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="5dp"
android:layout_above="@+id/playback_controls_top"
android:textColor="@android:color/white"
android:textSize="22sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:text="1:06:29"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/playback_controls_top"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="10dp"
android:layout_above="@+id/playback_controls_bottom"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/control_backward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_toLeftOf="@+id/control_play_pause"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_previous"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_play_pause"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_centerInParent="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/control_progress_bar"
style="?android:attr/progressBarStyleLargeInverse"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_centerInParent="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#00000000"
android:tint="?attr/colorAccent"
android:padding="2dp"
android:clickable="false"
android:scaleType="fitCenter"
android:indeterminate="true"
android:visibility="invisible"/>
<ImageButton
android:id="@+id/control_forward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toRightOf="@+id/control_play_pause"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/playback_controls_bottom"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="10dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/control_playback_speed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/control_repeat"
android:gravity="center"
android:minWidth="50dp"
android:text="1x"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"/>
<ImageButton
android:id="@+id/control_repeat"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toLeftOf="@+id/anchor"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_repeat_white"
tools:ignore="ContentDescription"/>
<View android:layout_width="10dp"
android:layout_height="1dp"
android:layout_centerInParent="true"
android:id="@+id/anchor"/>
<ImageButton
android:id="@+id/control_shuffle"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toRightOf="@+id/anchor"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_shuffle_white_24dp"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/control_playback_pitch"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/control_shuffle"
android:gravity="center"
android:minWidth="50dp"
android:text="100%"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"/>
</RelativeLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:background="@drawable/player_controls_bg"
android:paddingRight="16dp">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="40dp"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:06:29"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/seek_bar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingBottom="4dp"
android:paddingTop="8dp"
tools:progress="25"
tools:secondaryProgress="50"/>
<TextView
android:id="@+id/end_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:23:49"/>
</LinearLayout>
</RelativeLayout>

View File

@ -13,7 +13,6 @@
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<RelativeLayout
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/black"
android:gravity="center">
@ -11,6 +12,7 @@
android:id="@+id/aspectRatioLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center">
<SurfaceView
@ -40,6 +42,83 @@
tools:ignore="ContentDescription"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/playQueuePanel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="?attr/queue_background_color"
tools:visibility="visible">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/playQueueControl">
<ImageButton
android:id="@+id/playQueueClose"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="40dp"
android:layout_marginEnd="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/repeatButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="40dp"
android:layout_marginStart="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_repeat_off"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/shuffleButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/repeatButton"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/playQueue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/playQueueControl"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
@ -82,11 +161,16 @@
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONG"/>
@ -94,10 +178,15 @@
android:id="@+id/channelTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
android:clickable="true"
android:focusable="true"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
@ -135,26 +224,28 @@
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/repeatButton"
android:layout_toLeftOf="@+id/queueButton"
android:background="#00ffffff"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:scaleType="fitXY"
android:src="@drawable/ic_screen_rotation_white"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/repeatButton"
android:layout_width="35dp"
android:id="@+id/queueButton"
android:layout_width="30dp"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/fullScreenButton"
android:background="#00ffffff"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat_white"
android:src="@drawable/list"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
@ -165,6 +256,7 @@
android:layout_marginLeft="4dp"
android:background="#00ffffff"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_fullscreen_exit_white"
tools:ignore="ContentDescription,RtlHardcoded"/>
@ -223,11 +315,45 @@
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/playPreviousButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginRight="30dp"
android:layout_marginEnd="30dp"
android:layout_centerInParent="true"
android:layout_toLeftOf="@id/playPauseButton"
android:layout_toStartOf="@id/playPauseButton"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_previous"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/playNextButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_centerInParent="true"
android:layout_toRightOf="@id/playPauseButton"
android:layout_toEndOf="@id/playPauseButton"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
</RelativeLayout>
@ -328,4 +454,4 @@
tools:visibility="visible"/>
</RelativeLayout>
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,289 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_weight="1"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="@string/app_name"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/play_queue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/appbar"
android:layout_above="@id/center"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/center"
android:layout_above="@+id/playback_controls">
<LinearLayout
android:id="@+id/metadata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry">
<TextView
android:id="@+id/song_name"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textSize="14sp"
android:textColor="?attr/colorAccent"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta."
/>
<TextView
android:id="@+id/artist_name"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textSize="12sp"
tools:text="Duis posuere arcu condimentum lobortis mattis."/>
</LinearLayout>
<TextView
android:id="@+id/seek_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#c0000000"
android:paddingBottom="5dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="5dp"
android:textColor="@android:color/white"
android:textSize="22sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:text="1:06:29"
tools:visibility="visible"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:background="@drawable/player_controls_bg">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="40dp"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:06:29"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/seek_bar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
android:paddingTop="6dp"
tools:progress="25"
tools:secondaryProgress="50"/>
<TextView
android:id="@+id/end_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="-:--:--"
android:textColor="?attr/colorAccent"
tools:ignore="HardcodedText"
tools:text="1:23:49"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/playback_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/progress_bar"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/control_playback_speed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/control_repeat"
android:gravity="center"
android:minWidth="50dp"
android:text="1x"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"/>
<ImageButton
android:id="@+id/control_repeat"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toLeftOf="@+id/control_backward"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_repeat_white"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_backward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_toLeftOf="@+id/control_play_pause"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_previous"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_play_pause"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/control_progress_bar"
style="?android:attr/progressBarStyleLargeInverse"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_centerInParent="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#00000000"
android:tint="?attr/colorAccent"
android:padding="2dp"
android:clickable="false"
android:scaleType="fitCenter"
android:indeterminate="true"
android:visibility="invisible"
tools:visibility="visible"/>
<ImageButton
android:id="@+id/control_forward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toRightOf="@+id/control_play_pause"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/control_shuffle"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toRightOf="@+id/control_forward"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_shuffle_white_24dp"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/control_playback_pitch"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/control_shuffle"
android:gravity="center"
android:minWidth="50dp"
android:text="100%"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"/>
</RelativeLayout>
</RelativeLayout>

View File

@ -1,308 +1,368 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/video_item_detail"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_height="match_parent"
android:focusableInTouchMode="true">
<com.nirhart.parallaxscroll.views.ParallaxScrollView
android:id="@+id/detail_main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true">
android:visibility="visible"
app:parallax_factor="1.9">
<com.nirhart.parallaxscroll.views.ParallaxScrollView
android:id="@+id/detail_main_content"
<!--WRAPPER-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
app:parallax_factor="1.9">
android:layout_height="wrap_content"
android:orientation="vertical">
<!--WRAPPER-->
<LinearLayout
<!-- THUMBNAIL -->
<FrameLayout
android:id="@+id/detail_thumbnail_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:background="@android:color/black"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<!-- THUMBNAIL -->
<FrameLayout
android:id="@+id/detail_thumbnail_root_layout"
<ImageView
android:id="@+id/detail_thumbnail_image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/black"
android:background="@android:color/transparent"
android:contentDescription="@string/detail_thumbnail_view_description"
android:scaleType="centerCrop"
tools:ignore="RtlHardcoded"
tools:layout_height="200dp"
tools:src="@drawable/dummy_thumbnail"/>
<ImageView
android:id="@+id/detail_thumbnail_play_button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@android:color/transparent"
android:src="@drawable/new_play_arrow"
android:visibility="invisible"
tools:ignore="ContentDescription"
tools:visibility="visible"/>
<TextView
android:id="@+id/touch_append_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#64000000"
android:paddingBottom="10dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="10dp"
android:layout_gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold"
android:text="@string/hold_to_append"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
</FrameLayout>
<!-- CONTENT -->
<RelativeLayout
android:id="@+id/detail_content_root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground">
<!-- TITLE -->
<FrameLayout
android:id="@+id/detail_title_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:foreground="?attr/selectableItemBackground">
android:focusable="true"
android:paddingLeft="12dp"
android:paddingRight="12dp">
<ImageView
android:id="@+id/detail_thumbnail_image_view"
<TextView
android:id="@+id/detail_video_title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/detail_thumbnail_view_description"
android:scaleType="centerCrop"
android:layout_height="match_parent"
android:layout_marginRight="20dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="8dp"
android:paddingTop="12dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_title_text_size"
tools:ignore="RtlHardcoded"
tools:layout_height="200dp"
tools:src="@drawable/dummy_thumbnail"/>
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero."/>
<ImageView
android:id="@+id/detail_thumbnail_play_button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@android:color/transparent"
android:src="@drawable/new_play_arrow"
android:visibility="invisible"
tools:ignore="ContentDescription"
tools:visibility="visible"/>
android:id="@+id/detail_toggle_description_view"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_gravity="center_vertical|right"
android:layout_marginLeft="5dp"
android:src="@drawable/arrow_down"
tools:ignore="ContentDescription,RtlHardcoded"/>
</FrameLayout>
<!-- CONTENT -->
<RelativeLayout
android:id="@+id/detail_content_root_layout"
<!-- LOADING INDICATOR-->
<ProgressBar
android:id="@+id/loading_progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout"
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"/>
<!--ERROR PANEL-->
<include
android:id="@+id/error_panel"
layout="@layout/error_retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout"
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
android:visibility="gone"
tools:visibility="visible"/>
<!--HIDING ROOT-->
<LinearLayout
android:id="@+id/detail_content_root_hiding"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground">
android:layout_below="@+id/detail_title_root_layout"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<!-- TITLE -->
<FrameLayout
android:id="@+id/detail_title_root_layout"
<!--DETAIL-->
<RelativeLayout
android:id="@+id/detail_root"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginTop="6dp"
android:baselineAligned="false"
android:orientation="horizontal">
<!-- VIEW & THUMBS -->
<TextView
android:id="@+id/detail_view_count_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_views_text_size"
tools:ignore="RtlHardcoded"
tools:text="2,816,821,505 views"/>
<ImageView
android:id="@+id/detail_thumbs_up_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="?attr/thumbs_up"/>
<TextView
android:id="@+id/detail_thumbs_up_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_up_img_view"
android:gravity="center_vertical"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="12M"/>
<ImageView
android:id="@+id/detail_thumbs_down_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_thumbs_up_count_view"
android:contentDescription="@string/detail_dislikes_img_view_description"
android:src="?attr/thumbs_down"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/detail_thumbs_down_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_down_img_view"
android:gravity="center_vertical"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="10K"/>
<TextView
android:id="@+id/detail_thumbs_disabled_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_thumbs_down_img_view"
android:gravity="center_vertical"
android:text="@string/disabled"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
<!-- CONTROLS -->
<TextView
android:id="@+id/detail_controls_popup"
android:layout_width="80dp"
android:layout_height="55dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/open_in_popup_mode"
android:drawableTop="?attr/popup"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingTop="6dp"
android:text="@string/controls_popup_title"
android:textSize="12sp"/>
<TextView
android:id="@+id/detail_controls_background"
android:layout_width="80dp"
android:layout_height="55dp"
android:layout_alignParentTop="true"
android:layout_gravity="center_vertical"
android:layout_toLeftOf="@id/detail_controls_popup"
android:layout_toStartOf="@id/detail_controls_popup"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/play_audio"
android:drawableTop="?attr/audio"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingTop="6dp"
android:text="@string/controls_background_title"
android:textSize="12sp"/>
</RelativeLayout>
<!--UPLOADER-->
<LinearLayout
android:id="@+id/detail_uploader_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp">
android:paddingRight="12dp"
android:paddingTop="8dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/detail_uploader_thumbnail_view"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
android:src="@drawable/buddy"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/detail_video_title_view"
android:id="@+id/detail_uploader_text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="20dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="8dp"
android:paddingTop="12dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textStyle="bold"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero."/>
tools:text="Uploader"/>
<ImageView
android:id="@+id/detail_toggle_description_view"
android:layout_width="15dp"
android:layout_height="15dp"
<!--<Button
android:id="@+id/detail_uploader_subscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_marginLeft="5dp"
android:src="@drawable/arrow_down"
tools:ignore="ContentDescription,RtlHardcoded"/>
android:layout_marginRight="12dp"
android:text="@string/rss_button_title"
android:textSize="12sp"
android:theme="@style/RedButton"
android:drawableLeft="@drawable/ic_rss_feed_white_24dp"
tools:ignore="RtlHardcoded"
android:visibility="gone"/>-->
</LinearLayout>
</FrameLayout>
<!-- LOADING INDICATOR-->
<ProgressBar
android:id="@+id/loading_progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout"
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"/>
android:layout_height="1px"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:background="?attr/separator_color"/>
<!--ERROR PANEL-->
<include
android:id="@+id/error_panel"
layout="@layout/error_retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout"
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
android:visibility="gone"
tools:visibility="visible"/>
<!--HIDING ROOT-->
<!--DESCRIPTIONS-->
<LinearLayout
android:id="@+id/detail_content_root_hiding"
android:id="@+id/detail_description_root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/detail_title_root_layout"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<!--DETAIL-->
<RelativeLayout
android:id="@+id/detail_root"
android:layout_width="match_parent"
android:layout_height="55dp"
<TextView
android:id="@+id/detail_upload_date_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginTop="6dp"
android:baselineAligned="false"
android:orientation="horizontal">
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textStyle="bold"
tools:text="Published on Oct 2, 2009"/>
<!-- VIEW & THUMBS -->
<TextView
android:id="@+id/detail_view_count_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_views_text_size"
tools:ignore="RtlHardcoded"
tools:text="2,816,821,505 views"/>
<ImageView
android:id="@+id/detail_thumbs_up_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="?attr/thumbs_up"/>
<TextView
android:id="@+id/detail_thumbs_up_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_up_img_view"
android:gravity="center_vertical"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="12M"/>
<ImageView
android:id="@+id/detail_thumbs_down_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_thumbs_up_count_view"
android:contentDescription="@string/detail_dislikes_img_view_description"
android:src="?attr/thumbs_down"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/detail_thumbs_down_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_down_img_view"
android:gravity="center_vertical"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="10K"/>
<TextView
android:id="@+id/detail_thumbs_disabled_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detail_view_count_view"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_thumbs_down_img_view"
android:gravity="center_vertical"
android:text="@string/disabled"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
<!-- CONTROLS -->
<TextView
android:id="@+id/detail_controls_popup"
android:layout_width="80dp"
android:layout_height="55dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/open_in_popup_mode"
android:drawableTop="?attr/popup"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingTop="6dp"
android:text="@string/controls_popup_title"
android:textSize="12sp"/>
<TextView
android:id="@+id/detail_controls_background"
android:layout_width="80dp"
android:layout_height="55dp"
android:layout_alignParentTop="true"
android:layout_gravity="center_vertical"
android:layout_toLeftOf="@id/detail_controls_popup"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/play_audio"
android:drawableTop="?attr/audio"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingTop="6dp"
android:text="@string/controls_background_title"
android:textSize="12sp"/>
</RelativeLayout>
<!--UPLOADER-->
<LinearLayout
android:id="@+id/detail_uploader_root_layout"
<TextView
android:id="@+id/detail_description_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="8dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/detail_uploader_thumbnail_view"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
android:src="@drawable/buddy"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/detail_uploader_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textStyle="bold"
tools:ignore="RtlHardcoded"
tools:text="Uploader"/>
<!--<Button
android:id="@+id/detail_uploader_subscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_marginRight="12dp"
android:text="@string/rss_button_title"
android:textSize="12sp"
android:theme="@style/RedButton"
android:drawableLeft="@drawable/ic_rss_feed_white_24dp"
tools:ignore="RtlHardcoded"
android:visibility="gone"/>-->
</LinearLayout>
android:layout_marginBottom="8dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginTop="3dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="true"
android:textSize="@dimen/video_item_detail_description_text_size"
tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum."/>
<View
android:layout_width="match_parent"
@ -311,99 +371,50 @@
android:layout_marginRight="8dp"
android:background="?attr/separator_color"/>
<!--DESCRIPTIONS-->
<LinearLayout
android:id="@+id/detail_description_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/detail_upload_date_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textStyle="bold"
tools:text="Published on Oct 2, 2009"/>
<TextView
android:id="@+id/detail_description_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginTop="3dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textIsSelectable="true"
android:textSize="@dimen/video_item_detail_description_text_size"
tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum."/>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:background="?attr/separator_color"/>
</LinearLayout>
<!--NEXT AND RELATED VIDEOS-->
<LinearLayout
android:id="@+id/detail_related_streams_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="14dp"
android:orientation="vertical">
<TextView
android:id="@+id/detail_next_stream_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:text="@string/next_video_title"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_next_text_size"
tools:ignore="RtlHardcoded"/>
<LinearLayout
android:id="@+id/detail_related_streams_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="2dp"
android:orientation="vertical"
tools:minHeight="50dp"/>
<ImageButton
android:id="@+id/detail_related_streams_expand"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingBottom="10dp"
android:paddingTop="4dp"
android:src="?attr/expand"
android:textAlignment="center"
android:textAllCaps="true"
tools:ignore="ContentDescription"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</com.nirhart.parallaxscroll.views.ParallaxScrollView>
</FrameLayout>
<!--NEXT AND RELATED VIDEOS-->
<LinearLayout
android:id="@+id/detail_related_streams_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="14dp"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="?attr/toolbar_shadow_drawable"
android:layout_alignParentTop="true"/>
</RelativeLayout>
<TextView
android:id="@+id/detail_next_stream_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:text="@string/next_video_title"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_next_text_size"
tools:ignore="RtlHardcoded"/>
<LinearLayout
android:id="@+id/detail_related_streams_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="2dp"
android:orientation="vertical"
tools:minHeight="50dp"/>
<ImageButton
android:id="@+id/detail_related_streams_expand"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingBottom="10dp"
android:paddingTop="4dp"
android:src="?attr/expand"
android:textAlignment="center"
android:textAllCaps="true"
tools:ignore="ContentDescription"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</com.nirhart.parallaxscroll.views.ParallaxScrollView>
</FrameLayout>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="48dp"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selector_drawable"
android:clickable="true"
android:focusable="true"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<ImageView
android:id="@+id/itemSelected"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_centerInParent="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:scaleType="fitXY"
android:src="?attr/selected"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="@dimen/play_queue_thumbnail_width"
android:layout_height="@dimen/play_queue_thumbnail_height"
android:layout_toRightOf="@+id/itemSelected"
android:layout_toEndOf="@+id/itemSelected"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/>
<ImageView
android:id="@+id/itemHandle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_alignParentRight="true"
android:padding="@dimen/video_item_search_image_right_margin"
android:scaleType="center"
android:src="?attr/drag_handle"
tools:ignore="ContentDescription,RtlHardcoded"/>
<TextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_alignRight="@id/itemThumbnailView"
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
android:layout_marginRight="@dimen/video_item_search_duration_margin"
android:background="@color/duration_background_color"
android:paddingBottom="@dimen/video_item_search_duration_vertical_padding"
android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding"
android:paddingRight="@dimen/video_item_search_duration_horizontal_padding"
android:paddingTop="@dimen/video_item_search_duration_vertical_padding"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
tools:ignore="RtlHardcoded"
tools:text="1:09:10"/>
<TextView
android:id="@+id/itemVideoTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. "/>
<TextView
android:id="@+id/itemAdditionalDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
tools:text="Uploader"/>
</RelativeLayout>

View File

@ -12,6 +12,7 @@
android:layout_height="64dp"
android:background="@color/background_notification_color"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
@ -58,6 +59,7 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_repeat_white"
@ -69,9 +71,10 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_rewind"
android:src="@drawable/exo_controls_previous"
tools:ignore="ContentDescription"/>
<ImageButton
@ -80,6 +83,7 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
@ -89,9 +93,10 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_forward"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
<ImageButton
@ -101,6 +106,7 @@
android:layout_marginLeft="5dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp"

View File

@ -7,6 +7,7 @@
android:layout_height="128dp"
android:background="@color/background_notification_color"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
@ -26,6 +27,7 @@
android:layout_alignParentRight="true"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp"
@ -82,9 +84,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_alignTop="@+id/notificationProgressBar"
android:layout_toRightOf="@+id/notificationCover"
android:layout_toEndOf="@+id/notificationCover"
android:ellipsize="end"
android:maxLines="1"
android:textSize="12sp"
@ -109,6 +113,7 @@
android:layout_centerVertical="true"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat_white"
tools:ignore="ContentDescription"/>
@ -122,9 +127,10 @@
android:layout_toLeftOf="@+id/notificationPlayPause"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_rewind"
android:src="@drawable/exo_controls_previous"
tools:ignore="ContentDescription"/>
<ImageButton
@ -137,6 +143,7 @@
android:background="#00000000"
android:padding="2dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
@ -150,107 +157,10 @@
android:layout_marginRight="8dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_forward"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
</RelativeLayout>
</RelativeLayout>
<!--
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notificationContent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:background="@color/background_notification_color">
<ImageView
android:id="@+id/notificationCover"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginRight="8dp"
android:src="@drawable/dummy_thumbnail"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@+id/notificationButtons"
android:layout_toRightOf="@+id/notificationCover"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/notificationSongName"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="40dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="title" />
<TextView
android:id="@+id/notificationArtist"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:text="artist" />
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/playbackProgress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_marginRight="8dp" />
</LinearLayout>
<ImageButton
android:id="@+id/notificationStop"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:layout_margin="5dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_close_white" />
<RelativeLayout
android:id="@+id/notificationButtons"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignBottom="@id/notificationCover"
android:layout_alignParentRight="true"
android:layout_toRightOf="@+id/notificationCover"
android:orientation="horizontal" >
<ImageButton
android:id="@+id/notificationPlayPause"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_pause_white"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<ImageButton
android:id="@+id/notificationRewind"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_action_av_fast_rewind"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true" />
</RelativeLayout>
</RelativeLayout>-->
</RelativeLayout>

View File

@ -7,6 +7,7 @@
android:layout_height="64dp"
android:background="@color/background_notification_color"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
@ -54,6 +55,7 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_repeat_white"
@ -65,6 +67,7 @@
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
@ -75,6 +78,7 @@
android:layout_marginLeft="5dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp"

View File

@ -20,62 +20,125 @@
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/playlist_detail_title_text_size"
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
<RelativeLayout
android:id="@+id/uploader_layout"
android:layout_width="wrap_content"
android:layout_height="@dimen/playlist_detail_uploader_layout_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/playlist_title_view"
android:layout_marginLeft="4dp"
android:layout_marginRight="6dp"
android:layout_marginTop="6dp"
android:layout_toLeftOf="@+id/playlist_stream_count"
android:layout_toStartOf="@+id/playlist_stream_count"
android:background="?attr/selectableItemBackground"
android:gravity="left|center_vertical"
android:padding="2dp"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
android:id="@+id/playlist_meta">
<RelativeLayout
android:id="@+id/uploader_layout"
android:layout_width="wrap_content"
android:layout_height="@dimen/playlist_detail_uploader_layout_height"
android:layout_marginLeft="4dp"
android:layout_marginRight="6dp"
android:layout_marginTop="6dp"
android:layout_toLeftOf="@+id/playlist_stream_count"
android:layout_toStartOf="@+id/playlist_stream_count"
android:background="?attr/selectableItemBackground"
android:gravity="left|center_vertical"
android:padding="2dp"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/uploader_avatar_view"
android:layout_width="@dimen/playlist_detail_uploader_image_size"
android:layout_height="@dimen/playlist_detail_uploader_image_size"
android:layout_alignParentLeft="true"
android:layout_margin="1dp"
android:src="@drawable/buddy"
app:civ_border_color="#ffffff"
app:civ_border_width="1dp"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/uploader_avatar_view"
android:layout_width="@dimen/playlist_detail_uploader_image_size"
android:layout_height="@dimen/playlist_detail_uploader_image_size"
android:layout_alignParentLeft="true"
android:layout_margin="1dp"
android:src="@drawable/buddy"
app:civ_border_color="#ffffff"
app:civ_border_width="1dp"/>
<TextView
android:id="@+id/uploader_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@+id/uploader_avatar_view"
android:ellipsize="end"
android:gravity="left|center_vertical"
android:maxLines="1"
android:textSize="@dimen/playlist_detail_subtext_size"
tools:ignore="RtlHardcoded"
tools:text="Typical uploader name"/>
</RelativeLayout>
<TextView
android:id="@+id/uploader_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@+id/uploader_avatar_view"
android:id="@+id/playlist_stream_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/uploader_layout"
android:layout_alignTop="@+id/uploader_layout"
android:layout_alignParentRight="true"
android:layout_marginRight="6dp"
android:ellipsize="end"
android:gravity="left|center_vertical"
android:gravity="right|center_vertical"
android:maxLines="1"
android:textSize="@dimen/playlist_detail_subtext_size"
tools:ignore="RtlHardcoded"
tools:text="Typical uploader name"/>
tools:text="234 videos"/>
</RelativeLayout>
<TextView
android:id="@+id/playlist_stream_count"
android:layout_width="wrap_content"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/uploader_layout"
android:layout_alignEnd="@+id/playlist_title_view"
android:layout_alignRight="@+id/playlist_title_view"
android:layout_alignTop="@+id/uploader_layout"
android:ellipsize="end"
android:gravity="right|center_vertical"
android:maxLines="1"
android:textSize="@dimen/playlist_detail_subtext_size"
tools:ignore="RtlHardcoded"
tools:text="234 videos"/>
android:id="@+id/play_control"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:layout_below="@+id/playlist_meta">
<Button
android:id="@+id/playlist_play_bg_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/playlist_play_all_button"
android:layout_toStartOf="@+id/playlist_play_all_button"
android:text="@string/controls_background_title"
android:textSize="@dimen/channel_rss_title_size"
android:textColor="?attr/colorAccent"
android:theme="@style/RedButton"
android:drawableLeft="?attr/audio"
android:drawablePadding="4dp"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<Button
android:id="@+id/playlist_play_all_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/playlist_play_popup_button"
android:layout_toStartOf="@+id/playlist_play_popup_button"
android:text="@string/play_all"
android:textSize="@dimen/channel_rss_title_size"
android:textColor="?attr/colorAccent"
android:theme="@style/RedButton"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
<Button
android:id="@+id/playlist_play_popup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:layout_alignParentRight="true"
android:text="@string/controls_popup_title"
android:textSize="@dimen/channel_rss_title_size"
android:textColor="?attr/colorAccent"
android:theme="@style/RedButton"
android:drawableLeft="?attr/popup"
android:drawablePadding="4dp"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,20 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.schabi.newpipe.history.HistoryActivity">
<item android:id="@+id/action_history"
android:orderInCategory="981"
android:title="@string/action_history"
app:showAsAction="never"/>
<item android:id="@+id/action_settings"
android:orderInCategory="990"
android:title="@string/settings"
app:showAsAction="never"/>
<item android:id="@+id/action_system_audio"
android:orderInCategory="996"
android:title="@string/play_queue_audio_settings"
app:showAsAction="never"/>
</menu>

View File

@ -18,10 +18,14 @@
<attr name="palette" format="reference"/>
<attr name="language" format="reference"/>
<attr name="history" format="reference"/>
<attr name="drag_handle" format="reference"/>
<attr name="selected" format="reference"/>
<!-- Can't refer to colors directly into drawable's xml-->
<attr name="toolbar_shadow_drawable" format="reference"/>
<attr name="selector_drawable" format="reference"/>
<attr name="separator_color" format="color"/>
<attr name="queue_background_color" format="color"/>
<attr name="contrast_background_color" format="color"/>
</resources>

View File

@ -12,6 +12,7 @@
<color name="light_shadow_start_color">#5a000000</color>
<color name="light_license_background_color">#ffffff</color>
<color name="light_license_text_color">#212121</color>
<color name="light_queue_background_color">#c8ffffff</color>
<!-- Dark Theme -->
<color name="dark_background_color">#222222</color>
@ -24,6 +25,7 @@
<color name="dark_shadow_start_color">#82000000</color>
<color name="dark_license_background_color">#424242</color>
<color name="dark_license_text_color">#ffffff</color>
<color name="dark_queue_background_color">#af000000</color>
<!-- Black Theme -->
<color name="black_background_color">#000</color>
@ -44,6 +46,9 @@
<color name="subscribed_background_color">#d6d6d6</color>
<color name="subscribed_text_color">#717171</color>
<color name="transparent_background_color">#00000000</color>
<color name="selected_background_color">#96717171</color>
<!-- GigaGet theme -->
<color name="bluegray">#607D8B</color>

View File

@ -67,6 +67,10 @@
<dimen name="playlist_detail_uploader_image_size">24dp</dimen>
<dimen name="playlist_detail_uploader_layout_height">28dp</dimen>
<!-- Play Queue View Dimensions -->
<dimen name="play_queue_thumbnail_width">62dp</dimen>
<dimen name="play_queue_thumbnail_height">40dp</dimen>
<!-- Kiosk view Dimensions-->
<dimen name="kiosk_title_text_size">30sp</dimen>
</resources>

View File

@ -96,6 +96,7 @@
<string name="show_search_suggestions_key" translatable="false">show_search_suggestions</string>
<string name="show_play_with_kodi_key" translatable="false">show_play_with_kodi</string>
<string name="show_next_video_key" translatable="false">show_next_video</string>
<string name="show_hold_to_append_key" translatable="false">show_hold_to_append</string>
<string name="default_language_value">en</string>
<string name="search_language_key" translatable="false">search_language</string>
<string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string>

View File

@ -53,7 +53,7 @@
<string name="show_higher_resolutions_summary">Only some devices support playing 2K/4K videos</string>
<string name="play_with_kodi_title">Play with Kodi</string>
<string name="kore_not_found">Kore app not found. Install it?</string>
<string name="fdroid_kore_url" translatable="false">https://f-droid.org/repository/browse/?fdfilter=Kore&amp;fdid=org.xbmc.kore</string>
<string name="kore_package" translatable="false">org.xbmc.kore</string>
<string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string>
<string name="show_play_with_kodi_summary">Display an option to play a video via Kodi media center</string>
<string name="play_audio">Audio</string>
@ -80,6 +80,8 @@
<string name="download_dialog_title">Download</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_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="url_not_supported_toast">URL not supported</string>
<string name="search_language_title">Default content language</string>
<string name="settings_category_player_title">Player</string>
@ -91,6 +93,8 @@
<string name="settings_category_other_title">Other</string>
<string name="background_player_playing_toast">Playing in background</string>
<string name="popup_playing_toast">Playing in popup mode</string>
<string name="background_player_append">Queued on background player</string>
<string name="popup_playing_append">Queued on popup player</string>
<string name="c3s_url" translatable="false">https://www.c3s.cc/</string>
<string name="play_btn_text">Play</string>
<string name="content">Content</string>
@ -112,6 +116,7 @@
<string name="popup_resizing_indicator_title">Resizing</string>
<string name="best_resolution">Best resolution</string>
<string name="undo">Undo</string>
<string name="play_all">Play All</string>
<string name="notification_channel_id" translatable="false">newpipe</string>
<string name="notification_channel_name">NewPipe Notification</string>
@ -131,6 +136,10 @@
<string name="could_not_get_stream">Could not get any stream</string>
<string name="could_not_load_image">Could not load image</string>
<string name="app_ui_crash">App/UI crashed</string>
<string name="player_stream_failure">Failed to play this stream</string>
<string name="player_unrecoverable_failure">Unrecoverable player error occurred</string>
<string name="player_recoverable_failure">Recovering from player error</string>
<!-- error activity -->
<string name="sorry_string">Sorry, that should not have happened.</string>
<string name="guru_meditation" translatable="false">Guru Meditation.</string>
@ -291,4 +300,12 @@
<string name="top_50">Top 50</string>
<string name="new_and_hot">New &amp; hot</string>
<string name="service_kiosk_string" translatable="false">%1$s/%2$s</string>
<!-- Play Queue -->
<string name="title_activity_background_player">Background Player</string>
<string name="title_activity_popup_player">Popup Player</string>
<string name="play_queue_remove">Remove</string>
<string name="play_queue_stream_detail">Details</string>
<string name="play_queue_audio_settings">Audio Settings</string>
<string name="hold_to_append">Hold To Enqueue</string>
</resources>

View File

@ -25,10 +25,14 @@
<item name="palette">@drawable/ic_palette_black_24dp</item>
<item name="language">@drawable/ic_language_black_24dp</item>
<item name="history">@drawable/ic_history_black_24dp</item>
<item name="drag_handle">@drawable/ic_drag_handle_black_24dp</item>
<item name="selected">@drawable/ic_fiber_manual_record_black_24dp</item>
<item name="separator_color">@color/light_separator_color</item>
<item name="contrast_background_color">@color/light_contrast_background_color</item>
<item name="queue_background_color">@color/light_queue_background_color</item>
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_light</item>
<item name="selector_drawable">@drawable/light_selector</item>
<item name="colorControlHighlight">@color/light_ripple_color</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
@ -59,10 +63,14 @@
<item name="palette">@drawable/ic_palette_white_24dp</item>
<item name="language">@drawable/ic_language_white_24dp</item>
<item name="history">@drawable/ic_history_white_24dp</item>
<item name="drag_handle">@drawable/ic_drag_handle_white_24dp</item>
<item name="selected">@drawable/ic_fiber_manual_record_white_24dp</item>
<item name="separator_color">@color/dark_separator_color</item>
<item name="contrast_background_color">@color/dark_contrast_background_color</item>
<item name="queue_background_color">@color/dark_queue_background_color</item>
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_dark</item>
<item name="selector_drawable">@drawable/dark_selector</item>
<item name="colorControlHighlight">@color/dark_ripple_color</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>

View File

@ -16,4 +16,9 @@
android:key="@string/show_next_video_key"
android:title="@string/show_next_and_similar_title"/>
<SwitchPreference
android:defaultValue="true"
android:key="@string/show_hold_to_append_key"
android:title="@string/show_hold_to_append_title"
android:summary="@string/show_hold_to_append_summary"/>
</PreferenceScreen>