Merge remote-tracking branch 'origin/dev' into dev
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.schabi.newpipe.playlist.events;
|
||||
|
||||
public class InitEvent implements PlayQueueEvent {
|
||||
@Override
|
||||
public PlayQueueEventType type() {
|
||||
return PlayQueueEventType.INIT;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.schabi.newpipe.playlist.events;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface PlayQueueEvent extends Serializable {
|
||||
PlayQueueEventType type();
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package org.schabi.newpipe.playlist.events;
|
||||
|
||||
public class ReorderEvent implements PlayQueueEvent {
|
||||
@Override
|
||||
public PlayQueueEventType type() {
|
||||
return PlayQueueEventType.REORDER;
|
||||
}
|
||||
|
||||
public ReorderEvent() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 98 B |
After Width: | Height: | Size: 99 B |
After Width: | Height: | Size: 256 B |
After Width: | Height: | Size: 252 B |
After Width: | Height: | Size: 300 B |
After Width: | Height: | Size: 81 B |
After Width: | Height: | Size: 82 B |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 181 B |
After Width: | Height: | Size: 217 B |
After Width: | Height: | Size: 96 B |
After Width: | Height: | Size: 91 B |
After Width: | Height: | Size: 335 B |
After Width: | Height: | Size: 328 B |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 104 B |
After Width: | Height: | Size: 94 B |
After Width: | Height: | Size: 468 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 486 B |
After Width: | Height: | Size: 112 B |
After Width: | Height: | Size: 95 B |
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 614 B |
After Width: | Height: | Size: 517 B |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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&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 & 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|