1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-11-04 17:16:24 +00:00

Merge pull request #2907 from avently/unifiedplayer

Expandable player with unified UI
This commit is contained in:
Tobias Groza 2020-08-01 12:53:19 +02:00 committed by GitHub
commit d8b5549fd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 6693 additions and 5655 deletions

View File

@ -2,9 +2,9 @@ package org.schabi.newpipe.report;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;

View File

@ -43,8 +43,8 @@
</receiver>
<service
android:name=".player.BackgroundPlayer"
android:exported="false">
android:name=".player.MainPlayer"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
@ -52,25 +52,9 @@
<activity
android:name=".player.BackgroundPlayerActivity"
android:label="@string/title_activity_background_player"
android:label="@string/title_activity_play_queue"
android:launchMode="singleTask" />
<activity
android:name=".player.PopupVideoPlayerActivity"
android:label="@string/title_activity_popup_player"
android:launchMode="singleTask" />
<service
android:name=".player.PopupVideoPlayer"
android:exported="false" />
<activity
android:name=".player.MainVideoPlayer"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/VideoPlayerTheme" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/settings" />

View File

@ -4,11 +4,14 @@ import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.schabi.newpipe.R;
import java.lang.reflect.Field;
@ -20,6 +23,9 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
super(context, attrs);
}
private boolean allowScroll = true;
private final Rect globalRect = new Rect();
@Override
public boolean onRequestChildRectangleOnScreen(
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
@ -55,6 +61,15 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
final MotionEvent ev) {
final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel);
if (playQueue != null) {
final boolean visible = playQueue.getGlobalVisibleRect(globalRect);
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
allowScroll = false;
return false;
}
}
allowScroll = true;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// remove reference to old nested scrolling child
@ -68,6 +83,26 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final View directTargetChild,
final View target,
final int nestedScrollAxes,
final int type) {
return allowScroll && super.onStartNestedScroll(
parent, child, directTargetChild, target, nestedScrollAxes, type);
}
@Override
public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout,
@NonNull final AppBarLayout child,
@NonNull final View target, final float velocityX,
final float velocityY, final boolean consumed) {
return allowScroll && super.onNestedFling(
coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Nullable
private OverScroller getScrollerField() {
try {

View File

@ -29,6 +29,8 @@ import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -37,10 +39,10 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
@ -51,6 +53,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.navigation.NavigationView;
import org.schabi.newpipe.extractor.NewPipe;
@ -61,14 +64,18 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PeertubeHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
@ -137,7 +144,7 @@ public class MainActivity extends AppCompatActivity {
ErrorActivity.reportUiError(this, e);
}
if (AndroidTvUtils.isTv(this)) {
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
}
@ -518,13 +525,27 @@ public class MainActivity extends AppCompatActivity {
handleIntent(intent);
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
final Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof OnKeyDownListener
&& !bottomSheetHiddenOrCollapsed()) {
// Provide keyDown event to fragment which then sends this event
// to the main player service
return ((OnKeyDownListener) fragment).onKeyDown(keyCode)
|| super.onKeyDown(keyCode, event);
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
if (AndroidTvUtils.isTv(this)) {
if (DeviceUtils.isTv(this)) {
View drawerPanel = findViewById(R.id.navigation);
if (drawer.isDrawerOpen(drawerPanel)) {
drawer.closeDrawers();
@ -532,11 +553,32 @@ public class MainActivity extends AppCompatActivity {
}
}
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it
if (fragment instanceof BackPressable) {
if (((BackPressable) fragment).onBackPressed()) {
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
// interacts with a fragment inside fragment_holder so all back presses should be
// handled by it
if (bottomSheetHiddenOrCollapsed()) {
final Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it
if (fragment instanceof BackPressable) {
if (((BackPressable) fragment).onBackPressed()) {
return;
}
}
} else {
final Fragment fragmentPlayer = getSupportFragmentManager()
.findFragmentById(R.id.fragment_player_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it
if (fragmentPlayer instanceof BackPressable) {
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
final FrameLayout bottomSheetLayout =
findViewById(R.id.fragment_player_holder);
BottomSheetBehavior.from(bottomSheetLayout)
.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
return;
}
}
@ -563,7 +605,7 @@ public class MainActivity extends AppCompatActivity {
break;
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder);
.findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof VideoDetailFragment) {
((VideoDetailFragment) fragment).openDownloadDialog();
}
@ -615,10 +657,6 @@ public class MainActivity extends AppCompatActivity {
super.onCreateOptionsMenu(menu);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof VideoDetailFragment)) {
findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE);
}
if (!(fragment instanceof SearchFragment)) {
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
.setVisibility(View.GONE);
@ -660,6 +698,13 @@ public class MainActivity extends AppCompatActivity {
}
StateSaver.clearStateFiles();
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
// When user watch a video inside popup and then tries to open the video in main player
// while the app is closed he will see a blank fragment on place of kiosk.
// Let's open it first
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
NavigationHelper.openMainFragment(getSupportFragmentManager());
}
handleIntent(getIntent());
} else {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
@ -708,8 +753,14 @@ public class MainActivity extends AppCompatActivity {
case STREAM:
boolean autoPlay = intent
.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
final String intentCacheKey = intent
.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
final PlayQueue playQueue = intentCacheKey != null
? SerializedCache.getInstance()
.take(intentCacheKey, PlayQueue.class)
: null;
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(),
serviceId, url, title, autoPlay);
serviceId, url, title, autoPlay, playQueue);
break;
case CHANNEL:
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
@ -742,4 +793,17 @@ public class MainActivity extends AppCompatActivity {
ErrorActivity.reportUiError(this, e);
}
}
/*
* Utils
* */
private boolean bottomSheetHiddenOrCollapsed() {
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
BottomSheetBehavior.from(bottomSheetLayout);
final int sheetState = bottomSheetBehavior.getState();
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
}
}

View File

@ -44,7 +44,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
@ -347,7 +347,7 @@ public class RouterActivity extends AppCompatActivity {
alertDialog.show();
if (AndroidTvUtils.isTv(this)) {
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
@ -701,7 +701,7 @@ public class RouterActivity extends AppCompatActivity {
playQueue = new SinglePlayQueue((StreamInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue, true);
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
@ -716,7 +716,7 @@ public class RouterActivity extends AppCompatActivity {
: new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue, true);
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
@ -726,6 +726,11 @@ public class RouterActivity extends AppCompatActivity {
};
}
private void openMainPlayer(final PlayQueue playQueue, final Choice choice) {
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
choice.url, "", true, true);
}
@Override
public void onDestroy() {
super.onDestroy();

View File

@ -13,7 +13,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
@ -57,7 +57,7 @@ public class DownloadActivity extends AppCompatActivity {
}
});
if (AndroidTvUtils.isTv(this)) {
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
}

View File

@ -1,16 +1,29 @@
package org.schabi.newpipe.fragments.detail;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import java.io.Serializable;
class StackItem implements Serializable {
private final int serviceId;
private final String url;
private String url;
private String title;
private PlayQueue playQueue;
StackItem(final int serviceId, final String url, final String title) {
StackItem(final int serviceId, final String url,
final String title, final PlayQueue playQueue) {
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.playQueue = playQueue;
}
public void setUrl(final String url) {
this.url = url;
}
public void setPlayQueue(final PlayQueue queue) {
this.playQueue = queue;
}
public int getServiceId() {
@ -29,6 +42,10 @@ class StackItem implements Serializable {
return url;
}
public PlayQueue getPlayQueue() {
return playQueue;
}
@Override
public String toString() {
return getServiceId() + ":" + getUrl() + " > " + getTitle();

View File

@ -519,7 +519,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue(), false));
.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> NavigationHelper

View File

@ -318,7 +318,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.subscribe(getPlaylistBookmarkSubscriber());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->

View File

@ -48,7 +48,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
@ -482,16 +482,16 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
searchToolbarContainer.setAlpha(0f);
searchToolbarContainer.setAlpha(0.0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
searchToolbarContainer.animate()
.translationX(0)
.alpha(1f)
.alpha(1.0f)
.setDuration(200)
.setInterpolator(new DecelerateInterpolator()).start();
} else {
searchToolbarContainer.setTranslationX(0);
searchToolbarContainer.setAlpha(1f);
searchToolbarContainer.setAlpha(1.0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
}
}
@ -525,7 +525,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
if (AndroidTvUtils.isTv(getContext())) {
if (DeviceUtils.isTv(getContext())) {
showKeyboardSearch();
}
});

View File

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@ -126,7 +126,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemView.setOnLongClickListener(view -> {
if (AndroidTvUtils.isTv(itemBuilder.getContext())) {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText);

View File

@ -321,7 +321,7 @@ public class StatisticsPlaylistFragment
}
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->

View File

@ -492,7 +492,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->

View File

@ -40,7 +40,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.Dia
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.AndroidTvUtils
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper
class FeedGroupDialog : DialogFragment(), BackPressable {
@ -237,7 +237,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
toolbar_search_edit_text.setOnClickListener {
if (AndroidTvUtils.isTv(context)) {
if (DeviceUtils.isTv(context)) {
showKeyboardSearch()
}
}

View File

@ -1,684 +0,0 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* BackgroundPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* Service Background Player implementing {@link VideoPlayer}.
*
* @author mauriciocolli
*/
public final class BackgroundPlayer extends Service {
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_REPEAT
= "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
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";
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 SET_IMAGE_RESOURCE_METHOD = "setImageResource";
private static final String TAG = "BackgroundPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int NOTIFICATION_ID = 123789;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private BasePlayerImpl basePlayerImpl;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private SharedPreferences sharedPreferences;
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private PlayerEventListener activityListener;
private IBinder mBinder;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView;
private boolean shouldUpdateOnProgress;
private int timesNotificationUpdated;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup();
mBinder = new PlayerServiceBinder(basePlayerImpl);
shouldUpdateOnProgress = true;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], "
+ "flags = [" + flags + "], startId = [" + startId + "]");
}
basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (basePlayerImpl != null) {
basePlayerImpl.savePlaybackState();
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
mBinder = null;
basePlayerImpl = null;
stopForeground(true);
stopSelf();
}
private void onScreenOnOff(final boolean on) {
if (DEBUG) {
Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
}
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
notBuilder = createNotification();
timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_background_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_background_notification_expanded);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
basePlayerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(basePlayerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
if (basePlayerImpl == null) {
return;
}
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
// Starts background player activity -- attempts to unlock lockscreen
final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this);
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID, intent,
PendingIntent.FLAG_UPDATE_CURRENT));
if (basePlayerImpl.playQueue != null && basePlayerImpl.playQueue.size() > 1) {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_next);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
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_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
} else {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_fastforward);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
}
setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode());
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
private synchronized void updateNotification(final 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);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_all);
break;
}
}
//////////////////////////////////////////////////////////////////////////
protected class BasePlayerImpl extends BasePlayer {
@NonNull
private final AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
BasePlayerImpl(final Context context) {
super(context);
this.resolver = new AudioPlaybackResolver(context, dataSource);
}
@Override
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
}
@Override
public void handleIntent(final Intent intent) {
super.handleIntent(intent);
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());
}
/*//////////////////////////////////////////////////////////////////////////
// Thumbnail Loading
//////////////////////////////////////////////////////////////////////////*/
private void updateNotificationThumbnail() {
if (basePlayerImpl == null) {
return;
}
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover,
basePlayerImpl.getThumbnail());
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover,
basePlayerImpl.getThumbnail());
}
}
@Override
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
resetNotification();
updateNotificationThumbnail();
updateNotification(-1);
}
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
resetNotification();
updateNotificationThumbnail();
updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPrepared(final boolean playWhenReady) {
super.onPrepared(playWhenReady);
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlayback();
}
@Override
public void onMuteUnmuteButtonClicked() {
super.onMuteUnmuteButtonClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress) {
return;
}
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) {
updateNotificationThumbnail();
}
}
if (bigNotRemoteView != null) {
if (cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration,
currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime,
getTimeString(currentProgress) + " / " + cachedDurationString);
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration,
currentProgress, false);
}
updateNotification(-1);
}
@Override
public void onPlayPrevious() {
super.onPlayPrevious();
triggerProgressUpdate();
}
@Override
public void onPlayNext() {
super.onPlayNext();
triggerProgressUpdate();
}
@Override
public void destroy() {
super.destroy();
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
@Override
public void onLoadingChanged(final boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(final int i) {
resetNotification();
updateNotification(-1);
updatePlayback();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
resetNotification();
updateNotificationThumbnail();
updateNotification(-1);
updateMetadata();
}
@Override
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
return resolver.resolve(info);
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
onClose();
}
/*//////////////////////////////////////////////////////////////////////////
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(final PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(final PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
}
private void updateMetadata() {
if (activityListener != null && getCurrentMetadata() != null) {
activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
}
}
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), getPlaybackParameters());
}
}
private void updateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
}
private void stopActivityBinding() {
if (activityListener != null) {
activityListener.onServiceStopped();
activityListener = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast Receiver
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void setupBroadcastReceiver(final IntentFilter intentFltr) {
super.setupBroadcastReceiver(intentFltr);
intentFltr.addAction(ACTION_CLOSE);
intentFltr.addAction(ACTION_PLAY_PAUSE);
intentFltr.addAction(ACTION_REPEAT);
intentFltr.addAction(ACTION_PLAY_PREVIOUS);
intentFltr.addAction(ACTION_PLAY_NEXT);
intentFltr.addAction(ACTION_FAST_REWIND);
intentFltr.addAction(ACTION_FAST_FORWARD);
intentFltr.addAction(Intent.ACTION_SCREEN_ON);
intentFltr.addAction(Intent.ACTION_SCREEN_OFF);
intentFltr.addAction(Intent.ACTION_HEADSET_PLUG);
}
@Override
public void onBroadcastReceived(final 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:
onClose();
break;
case ACTION_PLAY_PAUSE:
onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
break;
case ACTION_PLAY_NEXT:
onPlayNext();
break;
case ACTION_PLAY_PREVIOUS:
onPlayPrevious();
break;
case ACTION_FAST_FORWARD:
onFastForward();
break;
case ACTION_FAST_REWIND:
onFastRewind();
break;
case Intent.ACTION_SCREEN_ON:
onScreenOnOff(true);
break;
case Intent.ACTION_SCREEN_OFF:
onScreenOnOff(false);
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
@Override
public void changeState(final int state) {
super.changeState(state);
updatePlayback();
}
@Override
public void onPlaying() {
super.onPlaying();
resetNotification();
updateNotificationThumbnail();
updateNotification(R.drawable.exo_controls_pause);
}
@Override
public void onPaused() {
super.onPaused();
resetNotification();
updateNotificationThumbnail();
updateNotification(R.drawable.exo_controls_play);
}
@Override
public void onCompleted() {
super.onCompleted();
resetNotification();
if (bigNotRemoteView != null) {
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
}
updateNotificationThumbnail();
updateNotification(R.drawable.ic_replay_white_24dp);
}
}
}

View File

@ -1,13 +1,13 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import static org.schabi.newpipe.player.BackgroundPlayer.ACTION_CLOSE;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "BackgroundPlayerActivity";
@ -19,25 +19,25 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_background_player);
return getResources().getString(R.string.title_activity_play_queue);
}
@Override
public Intent getBindIntent() {
return new Intent(this, BackgroundPlayer.class);
return new Intent(this, MainPlayer.class);
}
@Override
public void startPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this);
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) {
((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this);
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).removeActivityListener(this);
}
}
@ -56,18 +56,30 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
}
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startService(
getSwitchIntent(PopupVideoPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
NavigationHelper.playOnPopupPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
return false;
}
@Override
public Intent getPlayerShutdownIntent() {
return new Intent(ACTION_CLOSE);
public void setupMenu(final Menu menu) {
if (player == null) {
return;
}
menu.findItem(R.id.action_switch_popup)
.setVisible(!((VideoPlayerImpl) player).popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!((VideoPlayerImpl) player).audioPlayerSelected());
}
}

View File

@ -54,6 +54,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import io.reactivex.android.schedulers.AndroidSchedulers;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
@ -128,13 +129,15 @@ public abstract class BasePlayer implements
@NonNull
public static final String SELECT_ON_APPEND = "select_on_append";
@NonNull
public static final String PLAYER_TYPE = "player_type";
@NonNull
public static final String IS_MUTED = "is_muted";
/*//////////////////////////////////////////////////////////////////////////
// Playback
//////////////////////////////////////////////////////////////////////////*/
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
protected PlayQueue playQueue;
protected PlayQueueAdapter playQueueAdapter;
@ -159,6 +162,10 @@ public abstract class BasePlayer implements
protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
public static final int PLAYER_TYPE_VIDEO = 0;
public static final int PLAYER_TYPE_AUDIO = 1;
public static final int PLAYER_TYPE_POPUP = 2;
protected SimpleExoPlayer simpleExoPlayer;
protected AudioReactor audioReactor;
protected MediaSessionManager mediaSessionManager;
@ -223,7 +230,7 @@ public abstract class BasePlayer implements
public void setup() {
if (simpleExoPlayer == null) {
initPlayer(/*playOnInit=*/true);
initPlayer(true);
}
initListeners();
}
@ -250,7 +257,8 @@ public abstract class BasePlayer implements
registerBroadcastReceiver();
}
public void initListeners() { }
public void initListeners() {
}
public void handleIntent(final Intent intent) {
if (DEBUG) {
@ -288,34 +296,72 @@ public abstract class BasePlayer implements
final float playbackPitch = savedParameters.pitch;
final boolean playbackSkipSilence = savedParameters.skipSilence;
final boolean samePlayQueue = playQueue != null && playQueue.equals(queue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean isMuted = intent
.getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted());
/*
* There are 3 situations when playback shouldn't be started from scratch (zero timestamp):
* 1. User pressed on a timestamp link and the same video should be rewound to the timestamp
* 2. User changed a player from, for example. main to popup, or from audio to main, etc
* 3. User chose to resume a video based on a saved timestamp from history of played videos
* In those cases time will be saved because re-init of the play queue is a not an instant
* task and requires network calls
* */
// seek to timestamp if stream is already playing
if (simpleExoPlayer != null
&& queue.size() == 1
&& playQueue != null
&& playQueue.size() == 1
&& playQueue.getItem() != null
&& queue.getItem().getUrl().equals(playQueue.getItem().getUrl())
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET
) {
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
// Player can have state = IDLE when playback is stopped or failed
// and we should retry() in this case
if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) {
simpleExoPlayer.retry();
}
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
return;
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
} else if (samePlayQueue && !playQueue.isDisposed() && simpleExoPlayer != null) {
// Do not re-init the same PlayQueue. Save time
// Player can have state = IDLE when playback is stopped or failed
// and we should retry() in this case
if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) {
simpleExoPlayer.retry();
}
return;
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
&& isPlaybackResumeEnabled()
&& !samePlayQueue) {
final PlayQueueItem item = queue.getItem();
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
stateLoader = recordManager.loadStreamState(item)
.observeOn(mainThread())
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed,
playbackPitch, playbackSkipSilence, true, isMuted))
.observeOn(AndroidSchedulers.mainThread())
// Do not place initPlayback() in doFinally() because
// it restarts playback after destroy()
//.doFinally()
.subscribe(
state -> queue
.setRecovery(queue.getIndex(), state.getProgressTime()),
state -> {
queue.setRecovery(queue.getIndex(), state.getProgressTime());
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
},
error -> {
if (DEBUG) {
error.printStackTrace();
}
// In case any error we can start playback without history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
},
() -> {
// Completed but not found in history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
}
);
databaseUpdateReactor.add(stateLoader);
@ -323,8 +369,11 @@ public abstract class BasePlayer implements
}
}
// Good to go...
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
/*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false), isMuted);
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : queue, repeatMode,
playbackSpeed, playbackPitch, playbackSkipSilence,
!intent.getBooleanExtra(START_PAUSED, false),
isMuted);
}
private PlaybackParameters retrievePlaybackParametersFromPreferences() {
@ -410,6 +459,7 @@ public abstract class BasePlayer implements
databaseUpdateReactor.clear();
progressUpdateReactor.set(null);
ImageLoader.getInstance().stop();
}
/*//////////////////////////////////////////////////////////////////////////
@ -561,7 +611,8 @@ public abstract class BasePlayer implements
}
}
public void onPausedSeek() { }
public void onPausedSeek() {
}
public void onCompleted() {
if (DEBUG) {
@ -1089,6 +1140,7 @@ public abstract class BasePlayer implements
}
simpleExoPlayer.setPlayWhenReady(true);
savePlaybackState();
}
public void onPause() {
@ -1101,6 +1153,7 @@ public abstract class BasePlayer implements
audioReactor.abandonAudioFocus();
simpleExoPlayer.setPlayWhenReady(false);
savePlaybackState();
}
public void onPlayPause() {
@ -1433,6 +1486,10 @@ public abstract class BasePlayer implements
return simpleExoPlayer != null && simpleExoPlayer.isPlaying();
}
public boolean isLoading() {
return simpleExoPlayer != null && simpleExoPlayer.isLoading();
}
@Player.RepeatMode
public int getRepeatMode() {
return simpleExoPlayer == null
@ -1473,8 +1530,9 @@ public abstract class BasePlayer implements
/**
* Sets the playback parameters of the player, and also saves them to shared preferences.
* Speed and pitch are rounded up to 2 decimal places before being used or saved.
* @param speed the playback speed, will be rounded to up to 2 decimal places
* @param pitch the playback pitch, will be rounded to up to 2 decimal places
*
* @param speed the playback speed, will be rounded to up to 2 decimal places
* @param pitch the playback pitch, will be rounded to up to 2 decimal places
* @param skipSilence skip silence during playback
*/
public void setPlaybackParameters(final float speed, final float pitch,
@ -1490,11 +1548,11 @@ public abstract class BasePlayer implements
private void savePlaybackParametersToPreferences(final float speed, final float pitch,
final boolean skipSilence) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putFloat(context.getString(R.string.playback_speed_key), speed)
.putFloat(context.getString(R.string.playback_pitch_key), pitch)
.putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence)
.apply();
.edit()
.putFloat(context.getString(R.string.playback_speed_key), speed)
.putFloat(context.getString(R.string.playback_pitch_key), pitch)
.putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence)
.apply();
}
public PlayQueue getPlayQueue() {

View File

@ -0,0 +1,483 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
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.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* One service for all players.
*
* @author mauriciocolli
*/
public final class MainPlayer extends Service {
private static final String TAG = "MainPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private VideoPlayerImpl playerImpl;
private WindowManager windowManager;
private SharedPreferences sharedPreferences;
private final IBinder mBinder = new MainPlayer.LocalBinder();
public enum PlayerType {
VIDEO,
AUDIO,
POPUP
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
static final int NOTIFICATION_ID = 123789;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView;
static final String ACTION_CLOSE =
"org.schabi.newpipe.player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE =
"org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS =
"org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_REPEAT =
"org.schabi.newpipe.player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT =
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS =
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND =
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD =
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ThemeHelper.setTheme(this);
createView();
}
private void createView() {
final View layout = View.inflate(this, R.layout.player, null);
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(layout);
playerImpl.shouldUpdateOnProgress = true;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& playerImpl.playQueue == null) {
// Player is not working, no need to process media button's action
return START_NOT_STICKY;
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
showNotificationAndStartForeground();
}
playerImpl.handleIntent(intent);
if (playerImpl.mediaSessionManager != null) {
playerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
public void stop(final boolean autoplayEnabled) {
if (DEBUG) {
Log.d(TAG, "stop() called");
}
if (playerImpl.getPlayer() != null) {
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
// Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) {
playerImpl.onPause();
}
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
playerImpl.getPlayer().stop(false);
playerImpl.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
playerImpl.hideControls(0, 0);
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case
if (!autoplayEnabled) {
stopForeground(true);
}
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (playerImpl != null) {
removeViewFromParent();
playerImpl.setRecovery();
playerImpl.savePlaybackState();
playerImpl.stopActivityBinding();
playerImpl.removePopupFromView();
playerImpl.destroy();
}
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
stopForeground(true);
stopSelf();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (playerImpl != null
&& playerImpl.getParentActivity() != null)
? playerImpl.getParentActivity().getResources().getDisplayMetrics()
: getResources().getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
}
public View getView() {
if (playerImpl == null) {
return null;
}
return playerImpl.getRootView();
}
public void removeViewFromParent() {
if (getView().getParent() != null) {
if (playerImpl.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView());
} else {
// This means view was added by windowManager for popup player
windowManager.removeViewImmediate(getView());
}
}
}
private void showNotificationAndStartForeground() {
resetNotification();
if (getBigNotRemoteView() != null) {
getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
if (getNotRemoteView() != null) {
getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
startForeground(NOTIFICATION_ID, getNotBuilder().build());
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
void resetNotification() {
notBuilder = createNotification();
playerImpl.timesNotificationUpdated = 0;
}
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);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
final NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
playerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
playerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
// Don't show anything until player is playing
if (playerImpl == null) {
return;
}
remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
// Starts VideoDetailFragment or opens BackgroundPlayerActivity.
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID,
getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_next);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
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_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
} else {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_fastforward);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
}
setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode());
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
synchronized void updateNotification(final 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);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
playerImpl.timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
if (remoteViews == null) {
return;
}
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all);
break;
}
}
private Intent getIntentForNotification() {
final Intent intent;
if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show BackgroundPlayerActivity
intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext());
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return intent;
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
NotificationCompat.Builder getNotBuilder() {
return notBuilder;
}
RemoteViews getBigNotRemoteView() {
return bigNotRemoteView;
}
RemoteViews getNotRemoteView() {
return notRemoteView;
}
public class LocalBinder extends Binder {
public MainPlayer getService() {
return MainPlayer.this;
}
public VideoPlayerImpl getPlayer() {
return MainPlayer.this.playerImpl;
}
}
}

View File

@ -1,66 +0,0 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import static org.schabi.newpipe.player.PopupVideoPlayer.ACTION_CLOSE;
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);
}
}
@Override
public int getPlayerOptionMenuResource() {
return R.menu.menu_play_queue_popup;
}
@Override
public boolean onPlayerOptionSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startService(
getSwitchIntent(BackgroundPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
return true;
}
return false;
}
@Override
public Intent getPlayerShutdownIntent() {
return new Intent(ACTION_CLOSE);
}
}

View File

@ -27,17 +27,21 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
@ -110,7 +114,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
public abstract boolean onPlayerOptionSelected(MenuItem item);
public abstract Intent getPlayerShutdownIntent();
public abstract void setupMenu(Menu m);
////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle
////////////////////////////////////////////////////////////////////////////
@ -152,6 +156,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
}
// Allow to setup visibility of menuItems
@Override
public boolean onPrepareOptionsMenu(final Menu m) {
setupMenu(m);
return super.onPrepareOptionsMenu(m);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
@ -175,11 +186,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
case R.id.action_switch_main:
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startActivity(
getSwitchIntent(MainVideoPlayer.class)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()));
return true;
}
return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
@ -191,13 +200,22 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
unbind();
}
protected Intent getSwitchIntent(final Class clazz) {
protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) {
return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz,
this.player.getPlayQueue(), this.player.getRepeatMode(),
this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(), null, false, false, this.player.isMuted())
this.player.getPlaybackSkipSilence(),
null,
true,
!this.player.isPlaying(),
this.player.isMuted())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying());
.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM)
.putExtra(Constants.KEY_URL, this.player.getVideoUrl())
.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle())
.putExtra(Constants.KEY_SERVICE_ID,
this.player.getCurrentMetadata().getMetadata().getServiceId())
.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
}
////////////////////////////////////////////////////////////////////////////
@ -247,6 +265,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance();
} else if (service instanceof MainPlayer.LocalBinder) {
player = ((MainPlayer.LocalBinder) service).getPlayer();
}
if (player == null || player.getPlayQueue() == null
@ -500,7 +520,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return;
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag());
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag());
}
@Override
@ -571,6 +591,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onQueueUpdate(final PlayQueue queue) {
}
@Override
public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled,
final PlaybackParameters parameters) {
@ -610,7 +634,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public void onMetadataUpdate(final StreamInfo info) {
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
if (info != null) {
metadataTitle.setText(info.getName());
metadataArtist.setText(info.getUploaderName());

View File

@ -34,16 +34,16 @@ import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
@ -69,6 +69,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import java.util.ArrayList;
import java.util.List;
@ -117,8 +118,7 @@ public abstract class VideoPlayer extends BasePlayer
private View rootView;
private AspectRatioFrameLayout aspectRatioFrameLayout;
private SurfaceView surfaceView;
private ExpandableSurfaceView surfaceView;
private View surfaceForeground;
private View loadingPanel;
@ -135,7 +135,7 @@ public abstract class VideoPlayer extends BasePlayer
private TextView playbackLiveSync;
private TextView playbackSpeedTextView;
private View topControlsRoot;
private LinearLayout topControlsRoot;
private TextView qualityTextView;
private SubtitleView subtitleView;
@ -182,7 +182,6 @@ public abstract class VideoPlayer extends BasePlayer
public void initViews(final View view) {
this.rootView = view;
this.aspectRatioFrameLayout = view.findViewById(R.id.aspectRatioLayout);
this.surfaceView = view.findViewById(R.id.surfaceView);
this.surfaceForeground = view.findViewById(R.id.surfaceForeground);
this.loadingPanel = view.findViewById(R.id.loading_panel);
@ -207,12 +206,10 @@ public abstract class VideoPlayer extends BasePlayer
this.resizeView = view.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper
.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode()));
.resizeTypeOf(context, getSurfaceView().getResizeMode()));
this.captionTextView = view.findViewById(R.id.captionTextView);
//this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
}
@ -520,7 +517,6 @@ public abstract class VideoPlayer extends BasePlayer
super.onCompleted();
showControls(500);
animateView(endScreen, true, 800);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
loadingPanel.setVisibility(View.GONE);
@ -555,7 +551,7 @@ public abstract class VideoPlayer extends BasePlayer
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
}
aspectRatioFrameLayout.setAspectRatio(((float) width) / height);
getSurfaceView().setAspectRatio(((float) width) / height);
}
@Override
@ -620,12 +616,6 @@ public abstract class VideoPlayer extends BasePlayer
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
super.onPrepared(playWhenReady);
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler
.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
}
}
@Override
@ -675,7 +665,7 @@ public abstract class VideoPlayer extends BasePlayer
}
}
protected void onFullScreenButtonClicked() {
protected void toggleFullscreen() {
changeState(STATE_BLOCKED);
}
@ -799,16 +789,16 @@ public abstract class VideoPlayer extends BasePlayer
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onResizeClicked() {
if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
void onResizeClicked() {
if (getSurfaceView() != null) {
final int currentResizeMode = getSurfaceView().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode);
setResizeMode(newResizeMode);
}
}
protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
getAspectRatioFrameLayout().setResizeMode(resizeMode);
getSurfaceView().setResizeMode(resizeMode);
getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode));
}
@ -916,9 +906,9 @@ public abstract class VideoPlayer extends BasePlayer
if (drawableId == -1) {
if (controlAnimationView.getVisibility() == View.VISIBLE) {
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView,
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
).setDuration(DEFAULT_CONTROLS_DURATION);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
@ -1020,6 +1010,9 @@ public abstract class VideoPlayer extends BasePlayer
animateView(controlsRoot, false, duration);
};
}
public abstract void hideSystemUIIfNeeded();
/*//////////////////////////////////////////////////////////////////////////
// Getters and Setters
//////////////////////////////////////////////////////////////////////////*/
@ -1033,11 +1026,7 @@ public abstract class VideoPlayer extends BasePlayer
this.resolver.setPlaybackQuality(quality);
}
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
return aspectRatioFrameLayout;
}
public SurfaceView getSurfaceView() {
public ExpandableSurfaceView getSurfaceView() {
return surfaceView;
}
@ -1096,7 +1085,7 @@ public abstract class VideoPlayer extends BasePlayer
return playbackEndTime;
}
public View getTopControlsRoot() {
public LinearLayout getTopControlsRoot() {
return topControlsRoot;
}
@ -1108,6 +1097,10 @@ public abstract class VideoPlayer extends BasePlayer
return qualityPopupMenu;
}
public TextView getPlaybackSpeedTextView() {
return playbackSpeedTextView;
}
public PopupMenu getPlaybackSpeedPopupMenu() {
return playbackSpeedPopupMenu;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
package org.schabi.newpipe.player.event;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R;
import java.util.Arrays;
import java.util.List;
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
boolean visible;
Rect globalRect = new Rect();
private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
R.id.playQueuePanel, R.id.viewpager);
@Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final FrameLayout child,
final MotionEvent event) {
// Drop following when action ends
if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) {
skippingInterception = false;
}
// Found that user still swiping, continue following
if (skippingInterception) {
return false;
}
// Don't need to do anything if bottomSheet isn't expanded
if (getState() == BottomSheetBehavior.STATE_EXPANDED) {
// Without overriding scrolling will not work when user touches these elements
for (final Integer element : skipInterceptionOfElements) {
final ViewGroup viewGroup = child.findViewById(element);
if (viewGroup != null) {
visible = viewGroup.getGlobalVisibleRect(globalRect);
if (visible
&& globalRect.contains((int) event.getRawX(), (int) event.getRawY())) {
skippingInterception = true;
return false;
}
}
}
}
return super.onInterceptTouchEvent(parent, child, event);
}
}

View File

@ -0,0 +1,5 @@
package org.schabi.newpipe.player.event;
public interface OnKeyDownListener {
boolean onKeyDown(int keyCode);
}

View File

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

View File

@ -0,0 +1,622 @@
package org.schabi.newpipe.player.event;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.view.WindowManager;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl;
import org.schabi.newpipe.player.helper.PlayerHelper;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlayerGestureListener
extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener {
private static final String TAG = ".PlayerGestureListener";
private static final boolean DEBUG = BasePlayer.DEBUG;
private final VideoPlayerImpl playerImpl;
private final MainPlayer service;
private int initialPopupX;
private int initialPopupY;
private boolean isMovingInMain;
private boolean isMovingInPopup;
private boolean isResizing;
private final int tossFlingVelocity;
private final boolean isVolumeGestureEnabled;
private final boolean isBrightnessGestureEnabled;
private final int maxVolume;
private static final int MOVEMENT_THRESHOLD = 40;
// [popup] initial coordinates and distance between fingers
private double initPointerDistance = -1;
private float initFirstPointerX = -1;
private float initFirstPointerY = -1;
private float initSecPointerX = -1;
private float initSecPointerY = -1;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
this.playerImpl = playerImpl;
this.service = service;
this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service);
isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume();
}
/*//////////////////////////////////////////////////////////////////////////
// Helpers
//////////////////////////////////////////////////////////////////////////*/
/*
* Main and popup players' gesture listeners is too different.
* So it will be better to have different implementations of them
* */
@Override
public boolean onDoubleTap(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = "
+ e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
}
if (playerImpl.popupPlayerSelected()) {
return onDoubleTapInPopup(e);
} else {
return onDoubleTapInMain(e);
}
}
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onSingleTapConfirmedInPopup(e);
} else {
return onSingleTapConfirmedInMain(e);
}
}
@Override
public boolean onDown(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDown() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onDownInPopup(e);
} else {
return true;
}
}
@Override
public void onLongPress(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
onLongPressInPopup(e);
}
}
@Override
public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (playerImpl.popupPlayerSelected()) {
return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY);
} else {
return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY);
}
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2,
final float velocityX, final float velocityY) {
if (DEBUG) {
Log.d(TAG, "onFling() called with velocity: dX=["
+ velocityX + "], dY=[" + velocityY + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onFlingInPopup(e1, e2, velocityX, velocityY);
} else {
return true;
}
}
@Override
public boolean onTouch(final View v, final MotionEvent event) {
/*if (DEBUG && false) {
Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]");
}*/
if (playerImpl.popupPlayerSelected()) {
return onTouchInPopup(v, event);
} else {
return onTouchInMain(v, event);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Main player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInMain(final MotionEvent e) {
if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) {
playerImpl.onFastForward();
} else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) {
playerImpl.onFastRewind();
} else {
playerImpl.getPlayPauseButton().performClick();
}
return true;
}
private boolean onSingleTapConfirmedInMain(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) {
return true;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
} else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
playerImpl.showControls(0);
} else {
playerImpl.showControlsThenHide();
}
}
return true;
}
private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) {
return false;
}
final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service);
final boolean isTouchingNavigationBar = initialEvent.getY()
> playerImpl.getRootView().getHeight() - getNavigationBarHeight(service);
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false;
}
/*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " +
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]");*/
final boolean insideThreshold =
Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
|| playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return false;
}
isMovingInMain = true;
boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
boolean acceptVolumeArea = acceptAnyArea
|| initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0;
if (isVolumeGestureEnabled && acceptVolumeArea) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
playerImpl.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
playerImpl.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp
: R.drawable.ic_volume_up_white_24dp)
);
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
} else {
final Activity parent = playerImpl.getParentActivity();
if (parent == null) {
return true;
}
final Window window = parent.getWindow();
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar()
.getProgress() / playerImpl.getMaxGestureLength();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);
}
playerImpl.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low_white_24dp
: currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium_white_24dp
: R.drawable.ic_brightness_high_white_24dp)
);
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
return true;
}
private void onScrollEndInMain() {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
}
private boolean onTouchInMain(final View v, final MotionEvent event) {
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false;
onScrollEndInMain();
}
// This hack allows to stop receiving touch events on appbar
// while touching video player's view
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen());
return true;
case MotionEvent.ACTION_UP:
v.getParent().requestDisallowInterceptTouchEvent(false);
return false;
default:
return true;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Popup player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInPopup(final MotionEvent e) {
if (playerImpl == null || !playerImpl.isPlaying()) {
return false;
}
playerImpl.hideControls(0, 0);
if (e.getX() > playerImpl.getPopupWidth() / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
}
private boolean onSingleTapConfirmedInPopup(final MotionEvent e) {
if (playerImpl == null || playerImpl.getPlayer() == null) {
return false;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
}
return true;
}
private boolean onDownInPopup(final MotionEvent e) {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
initialPopupX = playerImpl.getPopupLayoutParams().x;
initialPopupY = playerImpl.getPopupLayoutParams().y;
playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width);
playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height);
return super.onDown(e);
}
private void onLongPressInPopup(final MotionEvent e) {
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1);
}
private boolean onScrollInPopup(final MotionEvent initialEvent,
final MotionEvent movingEvent,
final float distanceX,
final float distanceY) {
if (isResizing || playerImpl == null) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
}
if (!isMovingInPopup) {
animateView(playerImpl.getCloseOverlayButton(), true, 200);
}
isMovingInPopup = true;
final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX());
float posX = (int) (initialPopupX + diffX);
final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY());
float posY = (int) (initialPopupY + diffY);
if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) {
posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
} else if (posX < 0) {
posX = 0;
}
if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) {
posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
} else if (posY < 0) {
posY = 0;
}
playerImpl.getPopupLayoutParams().x = (int) posX;
playerImpl.getPopupLayoutParams().y = (int) posY;
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
}
}
// if (DEBUG) {
// Log.d(TAG, "onScrollInPopup = "
// + "e1.getRaw = [" + initialEvent.getRawX() + ", "
// + initialEvent.getRawY() + "], "
// + "e1.getX,Y = [" + initialEvent.getX() + ", "
// + initialEvent.getY() + "], "
// + "e2.getRaw = [" + movingEvent.getRawX() + ", "
// + movingEvent.getRawY() + "], "
// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], "
// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], "
// + "posX,Y = [" + posX + ", " + posY + "], "
// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]");
// }
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
private void onScrollEndInPopup(final MotionEvent event) {
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerImpl.isInsideClosingRadius(event)) {
playerImpl.closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) {
animateView(playerImpl.getCloseOverlayButton(), false, 200);
}
}
}
private boolean onFlingInPopup(final MotionEvent e1,
final MotionEvent e2,
final float velocityX,
final float velocityY) {
if (playerImpl == null) {
return false;
}
final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY);
if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().x = (int) velocityX;
}
if (absVelocityY > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().y = (int) velocityY;
}
playerImpl.checkPopupPositionBounds();
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
return false;
}
private boolean onTouchInPopup(final View v, final MotionEvent event) {
if (playerImpl == null) {
return false;
}
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
//record coordinates of fingers
initFirstPointerX = event.getX(0);
initFirstPointerY = event.getY(0);
initSecPointerX = event.getX(1);
initSecPointerY = event.getY(1);
//record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX,
initFirstPointerY - initSecPointerY);
isResizing = true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
return handleMultiDrag(event);
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
if (isMovingInPopup) {
isMovingInPopup = false;
onScrollEndInPopup(event);
}
if (isResizing) {
isResizing = false;
initPointerDistance = -1;
initFirstPointerX = -1;
initFirstPointerY = -1;
initSecPointerX = -1;
initSecPointerY = -1;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
if (!playerImpl.isPopupClosing) {
playerImpl.savePositionAndSize();
}
}
v.performClick();
return true;
}
private boolean handleMultiDrag(final MotionEvent event) {
if (initPointerDistance != -1 && event.getPointerCount() == 2) {
// get the movements of the fingers
double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX,
event.getY(0) - initFirstPointerY);
double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX,
event.getY(1) - initSecPointerY);
// minimum threshold beyond which pinch gesture will work
int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop();
if (Math.max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
final double currentPointerDistance =
Math.hypot(event.getX(0) - event.getX(1),
event.getY(0) - event.getY(1));
double popupWidth = playerImpl.getPopupWidth();
// change co-ordinates of popup so the center stays at the same position
double newWidth = (popupWidth * currentPointerDistance / initPointerDistance);
initPointerDistance = currentPointerDistance;
playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2;
playerImpl.checkPopupPositionBounds();
playerImpl.updateScreenSize();
playerImpl.updatePopupSize(
(int) Math.min(playerImpl.getScreenWidth(), newWidth),
-1);
return true;
}
}
return false;
}
/*
* Utils
* */
private int getNavigationBarHeight(final Context context) {
int resId = context.getResources()
.getIdentifier("navigation_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
private int getStatusBarHeight(final Context context) {
int resId = context.getResources()
.getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
}

View File

@ -0,0 +1,15 @@
package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.ExoPlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener {
void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked();
void onMoreOptionsLongClicked();
void onPlayerError(ExoPlaybackException error);
void hideSystemUiIfNeeded();
}

View File

@ -114,7 +114,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
private void onAudioFocusGain() {
Log.d(TAG, "onAudioFocusGain() called");
player.setVolume(DUCK_AUDIO_TO);
animateAudio(DUCK_AUDIO_TO, 1f);
animateAudio(DUCK_AUDIO_TO, 1.0f);
if (PlayerHelper.isResumeAfterAudioFocusGain(context)) {
player.setPlayWhenReady(true);

View File

@ -92,8 +92,10 @@ public class PlaybackParameterDialog extends DialogFragment {
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
final boolean playbackSkipSilence) {
final boolean playbackSkipSilence,
final Callback callback) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.callback = callback;
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
@ -111,9 +113,9 @@ public class PlaybackParameterDialog extends DialogFragment {
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context != null && context instanceof Callback) {
if (context instanceof Callback) {
callback = (Callback) context;
} else {
} else if (callback == null) {
dismiss();
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
@ -45,6 +46,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
@ -56,6 +60,15 @@ public final class PlayerHelper {
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
AUTOPLAY_TYPE_NEVER})
public @interface AutoplayType {
int AUTOPLAY_TYPE_ALWAYS = 0;
int AUTOPLAY_TYPE_WIFI = 1;
int AUTOPLAY_TYPE_NEVER = 2;
}
private PlayerHelper() { }
////////////////////////////////////////////////////////////////////////////
@ -203,6 +216,11 @@ public final class PlayerHelper {
return isAutoQueueEnabled(context, false);
}
public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.clear_queue_confirmation_key), false);
}
@MinimizeMode
public static int getMinimizeOnExitAction(@NonNull final Context context) {
final String defaultAction = context.getString(R.string.minimize_on_exit_none_key);
@ -219,6 +237,18 @@ public final class PlayerHelper {
}
}
@AutoplayType
public static int getAutoplayType(@NonNull final Context context) {
final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key));
if (type.equals(context.getString(R.string.autoplay_always_key))) {
return AUTOPLAY_TYPE_ALWAYS;
} else if (type.equals(context.getString(R.string.autoplay_never_key))) {
return AUTOPLAY_TYPE_NEVER;
} else {
return AUTOPLAY_TYPE_WIFI;
}
}
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
@ -308,7 +338,7 @@ public final class PlayerHelper {
final CaptioningManager captioningManager
= (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1f;
return 1.0f;
}
return captioningManager.getFontScale();
@ -324,6 +354,13 @@ public final class PlayerHelper {
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis());
}
public static boolean globalScreenOrientationLocked(final Context context) {
// 1: Screen orientation changes using accelerometer
// 0: Screen orientation is locked
return android.provider.Settings.System.getInt(
context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0;
}
////////////////////////////////////////////////////////////////////////////
// Private helpers
////////////////////////////////////////////////////////////////////////////
@ -396,6 +433,12 @@ public final class PlayerHelper {
.getString(context.getString(R.string.minimize_on_exit_key), key);
}
private static String getAutoplayType(@NonNull final Context context,
final String key) {
return getPreferences(context).getString(context.getString(R.string.autoplay_key),
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
final StreamInfoItem streamInfoItem) {
SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);

View File

@ -51,16 +51,24 @@ public abstract class PlayQueue implements Serializable {
@NonNull
private final AtomicInteger queueIndex;
private final ArrayList<PlayQueueItem> history;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
private transient boolean disposed;
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>();
streams.addAll(startWith);
history = new ArrayList<>();
if (streams.size() > index) {
history.add(streams.get(index));
}
queueIndex = new AtomicInteger(index);
disposed = false;
}
/*//////////////////////////////////////////////////////////////////////////
@ -99,6 +107,7 @@ public abstract class PlayQueue implements Serializable {
eventBroadcast = null;
broadcastReceiver = null;
reportingReactor = null;
disposed = true;
}
/**
@ -149,6 +158,9 @@ public abstract class PlayQueue implements Serializable {
if (index >= streams.size()) {
newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
}
if (oldIndex != newIndex) {
history.add(streams.get(newIndex));
}
queueIndex.set(newIndex);
broadcast(new SelectEvent(oldIndex, newIndex));
@ -269,7 +281,7 @@ public abstract class PlayQueue implements Serializable {
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
List<PlayQueueItem> itemList = new ArrayList<>(items);
final List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
backup.addAll(itemList);
@ -314,6 +326,9 @@ public abstract class PlayQueue implements Serializable {
public synchronized void error() {
final int oldIndex = getIndex();
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ErrorEvent(oldIndex, getIndex()));
}
@ -334,7 +349,11 @@ public abstract class PlayQueue implements Serializable {
if (backup != null) {
backup.remove(getItem(removeIndex));
}
streams.remove(removeIndex);
history.remove(streams.remove(removeIndex));
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
}
/**
@ -367,7 +386,7 @@ public abstract class PlayQueue implements Serializable {
queueIndex.incrementAndGet();
}
PlayQueueItem playQueueItem = streams.remove(source);
final PlayQueueItem playQueueItem = streams.remove(source);
playQueueItem.setAutoQueued(false);
streams.add(target, playQueueItem);
broadcast(new MoveEvent(source, target));
@ -427,6 +446,9 @@ public abstract class PlayQueue implements Serializable {
streams.add(0, streams.remove(newIndex));
}
queueIndex.set(0);
if (streams.size() > 0) {
history.add(streams.get(0));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
@ -458,10 +480,60 @@ public abstract class PlayQueue implements Serializable {
} else {
queueIndex.set(0);
}
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
* Selects previous played item.
*
* This method removes currently playing item from history and
* starts playing the last item from history if it exists
*
* @return true if history is not empty and the item can be played
* */
public synchronized boolean previous() {
if (history.size() <= 1) {
return false;
}
history.remove(history.size() - 1);
final PlayQueueItem last = history.remove(history.size() - 1);
setIndex(indexOf(last));
return true;
}
/*
* Compares two PlayQueues. Useful when a user switches players but queue is the same so
* we don't have to do anything with new queue.
* This method also gives a chance to track history of items in a queue in
* VideoDetailFragment without duplicating items from two identical queues
* */
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof PlayQueue)
|| getStreams().size() != ((PlayQueue) obj).getStreams().size()) {
return false;
}
final PlayQueue other = (PlayQueue) obj;
for (int i = 0; i < getStreams().size(); i++) {
if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) {
return false;
}
}
return true;
}
public boolean isDisposed() {
return disposed;
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/

View File

@ -13,7 +13,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
@ -62,7 +62,7 @@ public class SettingsActivity extends AppCompatActivity
.commit();
}
if (AndroidTvUtils.isTv(this)) {
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
}

View File

@ -1,416 +1,416 @@
package org.schabi.newpipe.streams;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment;
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* @author kapodamy
*/
public class OggFromWebMWriter implements Closeable {
private static final byte FLAG_UNSET = 0x00;
//private static final byte FLAG_CONTINUED = 0x01;
private static final byte FLAG_FIRST = 0x02;
private static final byte FLAG_LAST = 0x04;
private static final byte HEADER_CHECKSUM_OFFSET = 22;
private static final byte HEADER_SIZE = 27;
private static final int TIME_SCALE_NS = 1000000000;
private boolean done = false;
private boolean parsed = false;
private SharpStream source;
private SharpStream output;
private int sequenceCount = 0;
private final int streamId;
private byte packetFlag = FLAG_FIRST;
private WebMReader webm = null;
private WebMTrack webmTrack = null;
private Segment webmSegment = null;
private Cluster webmCluster = null;
private SimpleBlock webmBlock = null;
private long webmBlockLastTimecode = 0;
private long webmBlockNearDuration = 0;
private short segmentTableSize = 0;
private final byte[] segmentTable = new byte[255];
private long segmentTableNextTimestamp = TIME_SCALE_NS;
private final int[] crc32Table = new int[256];
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
if (!source.canRead() || !source.canRewind()) {
throw new IllegalArgumentException("source stream must be readable and allows seeking");
}
if (!target.canWrite() || !target.canRewind()) {
throw new IllegalArgumentException("output stream must be writable and allows seeking");
}
this.source = source;
this.output = target;
this.streamId = (int) System.currentTimeMillis();
populateCrc32Table();
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
return webm.getAvailableTracks();
}
public void parseSource() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
webm = new WebMReader(source);
webm.parse();
webmSegment = webm.getNextSegment();
} finally {
parsed = true;
}
}
public void selectTrack(final int trackIndex) throws IOException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
if (done) {
throw new IOException("already done");
}
if (webmTrack != null) {
throw new IOException("tracks already selected");
}
switch (webm.getAvailableTracks()[trackIndex].kind) {
case Audio:
case Video:
break;
default:
throw new UnsupportedOperationException("the track must an audio or video stream");
}
try {
webmTrack = webm.selectTrack(trackIndex);
} finally {
parsed = true;
}
}
@Override
public void close() throws IOException {
done = true;
parsed = true;
webmTrack = null;
webm = null;
if (!output.isClosed()) {
output.flush();
}
source.close();
output.close();
}
public void build() throws IOException {
float resolution;
SimpleBlock bloq;
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
header.order(ByteOrder.LITTLE_ENDIAN);
/* step 1: get the amount of frames per seconds */
switch (webmTrack.kind) {
case Audio:
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
if (resolution == 0f) {
throw new RuntimeException("cannot get the audio sample rate");
}
break;
case Video:
// WARNING: untested
if (webmTrack.defaultDuration == 0) {
throw new RuntimeException("missing default frame time");
}
resolution = 1000f / ((float) webmTrack.defaultDuration
/ webmSegment.info.timecodeScale);
break;
default:
throw new RuntimeException("not implemented");
}
/* step 2: create packet with code init data */
if (webmTrack.codecPrivate != null) {
addPacketSegment(webmTrack.codecPrivate.length);
makePacketheader(0x00, header, webmTrack.codecPrivate);
write(header);
output.write(webmTrack.codecPrivate);
}
/* step 3: create packet with metadata */
byte[] buffer = makeMetadata();
if (buffer != null) {
addPacketSegment(buffer.length);
makePacketheader(0x00, header, buffer);
write(header);
output.write(buffer);
}
/* step 4: calculate amount of packets */
while (webmSegment != null) {
bloq = getNextBlock();
if (bloq != null && addPacketSegment(bloq)) {
int pos = page.position();
//noinspection ResultOfMethodCallIgnored
bloq.data.read(page.array(), pos, bloq.dataSize);
page.position(pos + bloq.dataSize);
continue;
}
// calculate the current packet duration using the next block
double elapsedNs = webmTrack.codecDelay;
if (bloq == null) {
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
elapsedNs += webmBlockLastTimecode;
if (webmTrack.defaultDuration > 0) {
elapsedNs += webmTrack.defaultDuration;
} else {
// hardcoded way, guess the sample duration
elapsedNs += webmBlockNearDuration;
}
} else {
elapsedNs += bloq.absoluteTimeCodeNs;
}
// get the sample count in the page
elapsedNs = elapsedNs / TIME_SCALE_NS;
elapsedNs = Math.ceil(elapsedNs * resolution);
// create header and calculate page checksum
int checksum = makePacketheader((long) elapsedNs, header, null);
checksum = calcCrc32(checksum, page.array(), page.position());
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
// dump data
write(header);
write(page);
webmBlock = bloq;
}
}
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
final byte[] immediatePage) {
short length = HEADER_SIZE;
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
buffer.put((byte) 0x00); // version
buffer.put(packetFlag); // type
buffer.putLong(granPos); // granulate position
buffer.putInt(streamId); // bitstream serial number
buffer.putInt(sequenceCount++); // page sequence number
buffer.putInt(0x00); // page checksum
buffer.put((byte) segmentTableSize); // segment table
buffer.put(segmentTable, 0, segmentTableSize); // segment size
length += segmentTableSize;
clearSegmentTable(); // clear segment table for next header
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
if (immediatePage != null) {
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
segmentTableNextTimestamp -= TIME_SCALE_NS;
}
return checksumCrc32;
}
@Nullable
private byte[] makeMetadata() {
if ("A_OPUS".equals(webmTrack.codecId)) {
return new byte[]{
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
return new byte[]{
0x03, // ¿¿¿???
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
}
// not implemented for the desired codec
return null;
}
private void write(final ByteBuffer buffer) throws IOException {
output.write(buffer.array(), 0, buffer.position());
buffer.position(0);
}
@Nullable
private SimpleBlock getNextBlock() throws IOException {
SimpleBlock res;
if (webmBlock != null) {
res = webmBlock;
webmBlock = null;
return res;
}
if (webmSegment == null) {
webmSegment = webm.getNextSegment();
if (webmSegment == null) {
return null; // no more blocks in the selected track
}
}
if (webmCluster == null) {
webmCluster = webmSegment.getNextCluster();
if (webmCluster == null) {
webmSegment = null;
return getNextBlock();
}
}
res = webmCluster.getNextSimpleBlock();
if (res == null) {
webmCluster = null;
return getNextBlock();
}
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
webmBlockLastTimecode = res.absoluteTimeCodeNs;
return res;
}
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
// hardcoded way
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
while (buffer.remaining() >= 6) {
int id = buffer.getShort() & 0xFFFF;
if (id == 0x0000B584) {
return buffer.getFloat();
}
}
return 0f;
}
private void clearSegmentTable() {
segmentTableNextTimestamp += TIME_SCALE_NS;
packetFlag = FLAG_UNSET;
segmentTableSize = 0;
}
private boolean addPacketSegment(final SimpleBlock block) {
long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
if (timestamp >= segmentTableNextTimestamp) {
return false;
}
return addPacketSegment(block.dataSize);
}
private boolean addPacketSegment(final int size) {
if (size > 65025) {
throw new UnsupportedOperationException("page size cannot be larger than 65025");
}
int available = (segmentTable.length - segmentTableSize) * 255;
boolean extra = (size % 255) == 0;
if (extra) {
// add a zero byte entry in the table
// required to indicate the sample size is multiple of 255
available -= 255;
}
// check if possible add the segment, without overflow the table
if (available < size) {
return false; // not enough space on the page
}
for (int seg = size; seg > 0; seg -= 255) {
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
}
if (extra) {
segmentTable[segmentTableSize++] = 0x00;
}
return true;
}
private void populateCrc32Table() {
for (int i = 0; i < 0x100; i++) {
int crc = i << 24;
for (int j = 0; j < 8; j++) {
long b = crc >>> 31;
crc <<= 1;
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
}
crc32Table[i] = crc;
}
}
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
int crc = initialCrc;
for (int i = 0; i < size; i++) {
int reg = (crc >>> 24) & 0xff;
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
}
return crc;
}
}
package org.schabi.newpipe.streams;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.streams.WebMReader.Cluster;
import org.schabi.newpipe.streams.WebMReader.Segment;
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* @author kapodamy
*/
public class OggFromWebMWriter implements Closeable {
private static final byte FLAG_UNSET = 0x00;
//private static final byte FLAG_CONTINUED = 0x01;
private static final byte FLAG_FIRST = 0x02;
private static final byte FLAG_LAST = 0x04;
private static final byte HEADER_CHECKSUM_OFFSET = 22;
private static final byte HEADER_SIZE = 27;
private static final int TIME_SCALE_NS = 1000000000;
private boolean done = false;
private boolean parsed = false;
private SharpStream source;
private SharpStream output;
private int sequenceCount = 0;
private final int streamId;
private byte packetFlag = FLAG_FIRST;
private WebMReader webm = null;
private WebMTrack webmTrack = null;
private Segment webmSegment = null;
private Cluster webmCluster = null;
private SimpleBlock webmBlock = null;
private long webmBlockLastTimecode = 0;
private long webmBlockNearDuration = 0;
private short segmentTableSize = 0;
private final byte[] segmentTable = new byte[255];
private long segmentTableNextTimestamp = TIME_SCALE_NS;
private final int[] crc32Table = new int[256];
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
if (!source.canRead() || !source.canRewind()) {
throw new IllegalArgumentException("source stream must be readable and allows seeking");
}
if (!target.canWrite() || !target.canRewind()) {
throw new IllegalArgumentException("output stream must be writable and allows seeking");
}
this.source = source;
this.output = target;
this.streamId = (int) System.currentTimeMillis();
populateCrc32Table();
}
public boolean isDone() {
return done;
}
public boolean isParsed() {
return parsed;
}
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
return webm.getAvailableTracks();
}
public void parseSource() throws IOException, IllegalStateException {
if (done) {
throw new IllegalStateException("already done");
}
if (parsed) {
throw new IllegalStateException("already parsed");
}
try {
webm = new WebMReader(source);
webm.parse();
webmSegment = webm.getNextSegment();
} finally {
parsed = true;
}
}
public void selectTrack(final int trackIndex) throws IOException {
if (!parsed) {
throw new IllegalStateException("source must be parsed first");
}
if (done) {
throw new IOException("already done");
}
if (webmTrack != null) {
throw new IOException("tracks already selected");
}
switch (webm.getAvailableTracks()[trackIndex].kind) {
case Audio:
case Video:
break;
default:
throw new UnsupportedOperationException("the track must an audio or video stream");
}
try {
webmTrack = webm.selectTrack(trackIndex);
} finally {
parsed = true;
}
}
@Override
public void close() throws IOException {
done = true;
parsed = true;
webmTrack = null;
webm = null;
if (!output.isClosed()) {
output.flush();
}
source.close();
output.close();
}
public void build() throws IOException {
float resolution;
SimpleBlock bloq;
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
header.order(ByteOrder.LITTLE_ENDIAN);
/* step 1: get the amount of frames per seconds */
switch (webmTrack.kind) {
case Audio:
resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata);
if (resolution == 0f) {
throw new RuntimeException("cannot get the audio sample rate");
}
break;
case Video:
// WARNING: untested
if (webmTrack.defaultDuration == 0) {
throw new RuntimeException("missing default frame time");
}
resolution = 1000f / ((float) webmTrack.defaultDuration
/ webmSegment.info.timecodeScale);
break;
default:
throw new RuntimeException("not implemented");
}
/* step 2: create packet with code init data */
if (webmTrack.codecPrivate != null) {
addPacketSegment(webmTrack.codecPrivate.length);
makePacketheader(0x00, header, webmTrack.codecPrivate);
write(header);
output.write(webmTrack.codecPrivate);
}
/* step 3: create packet with metadata */
byte[] buffer = makeMetadata();
if (buffer != null) {
addPacketSegment(buffer.length);
makePacketheader(0x00, header, buffer);
write(header);
output.write(buffer);
}
/* step 4: calculate amount of packets */
while (webmSegment != null) {
bloq = getNextBlock();
if (bloq != null && addPacketSegment(bloq)) {
int pos = page.position();
//noinspection ResultOfMethodCallIgnored
bloq.data.read(page.array(), pos, bloq.dataSize);
page.position(pos + bloq.dataSize);
continue;
}
// calculate the current packet duration using the next block
double elapsedNs = webmTrack.codecDelay;
if (bloq == null) {
packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed
elapsedNs += webmBlockLastTimecode;
if (webmTrack.defaultDuration > 0) {
elapsedNs += webmTrack.defaultDuration;
} else {
// hardcoded way, guess the sample duration
elapsedNs += webmBlockNearDuration;
}
} else {
elapsedNs += bloq.absoluteTimeCodeNs;
}
// get the sample count in the page
elapsedNs = elapsedNs / TIME_SCALE_NS;
elapsedNs = Math.ceil(elapsedNs * resolution);
// create header and calculate page checksum
int checksum = makePacketheader((long) elapsedNs, header, null);
checksum = calcCrc32(checksum, page.array(), page.position());
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
// dump data
write(header);
write(page);
webmBlock = bloq;
}
}
private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer,
final byte[] immediatePage) {
short length = HEADER_SIZE;
buffer.putInt(0x5367674f); // "OggS" binary string in little-endian
buffer.put((byte) 0x00); // version
buffer.put(packetFlag); // type
buffer.putLong(granPos); // granulate position
buffer.putInt(streamId); // bitstream serial number
buffer.putInt(sequenceCount++); // page sequence number
buffer.putInt(0x00); // page checksum
buffer.put((byte) segmentTableSize); // segment table
buffer.put(segmentTable, 0, segmentTableSize); // segment size
length += segmentTableSize;
clearSegmentTable(); // clear segment table for next header
int checksumCrc32 = calcCrc32(0x00, buffer.array(), length);
if (immediatePage != null) {
checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length);
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32);
segmentTableNextTimestamp -= TIME_SCALE_NS;
}
return checksumCrc32;
}
@Nullable
private byte[] makeMetadata() {
if ("A_OPUS".equals(webmTrack.codecId)) {
return new byte[]{
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
return new byte[]{
0x03, // ¿¿¿???
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string
0x00, 0x00, 0x00, 0x00, // writing application string size (not present)
0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags)
};
}
// not implemented for the desired codec
return null;
}
private void write(final ByteBuffer buffer) throws IOException {
output.write(buffer.array(), 0, buffer.position());
buffer.position(0);
}
@Nullable
private SimpleBlock getNextBlock() throws IOException {
SimpleBlock res;
if (webmBlock != null) {
res = webmBlock;
webmBlock = null;
return res;
}
if (webmSegment == null) {
webmSegment = webm.getNextSegment();
if (webmSegment == null) {
return null; // no more blocks in the selected track
}
}
if (webmCluster == null) {
webmCluster = webmSegment.getNextCluster();
if (webmCluster == null) {
webmSegment = null;
return getNextBlock();
}
}
res = webmCluster.getNextSimpleBlock();
if (res == null) {
webmCluster = null;
return getNextBlock();
}
webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode;
webmBlockLastTimecode = res.absoluteTimeCodeNs;
return res;
}
private float getSampleFrequencyFromTrack(final byte[] bMetadata) {
// hardcoded way
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
while (buffer.remaining() >= 6) {
int id = buffer.getShort() & 0xFFFF;
if (id == 0x0000B584) {
return buffer.getFloat();
}
}
return 0.0f;
}
private void clearSegmentTable() {
segmentTableNextTimestamp += TIME_SCALE_NS;
packetFlag = FLAG_UNSET;
segmentTableSize = 0;
}
private boolean addPacketSegment(final SimpleBlock block) {
long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay;
if (timestamp >= segmentTableNextTimestamp) {
return false;
}
return addPacketSegment(block.dataSize);
}
private boolean addPacketSegment(final int size) {
if (size > 65025) {
throw new UnsupportedOperationException("page size cannot be larger than 65025");
}
int available = (segmentTable.length - segmentTableSize) * 255;
boolean extra = (size % 255) == 0;
if (extra) {
// add a zero byte entry in the table
// required to indicate the sample size is multiple of 255
available -= 255;
}
// check if possible add the segment, without overflow the table
if (available < size) {
return false; // not enough space on the page
}
for (int seg = size; seg > 0; seg -= 255) {
segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255);
}
if (extra) {
segmentTable[segmentTableSize++] = 0x00;
}
return true;
}
private void populateCrc32Table() {
for (int i = 0; i < 0x100; i++) {
int crc = i << 24;
for (int j = 0; j < 8; j++) {
long b = crc >>> 31;
crc <<= 1;
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
}
crc32Table[i] = crc;
}
}
private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) {
int crc = initialCrc;
for (int i = 0; i < size; i++) {
int reg = (crc >>> 24) & 0xff;
crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)];
}
return crc;
}
}

View File

@ -8,22 +8,23 @@ import android.os.BatteryManager;
import android.os.Build;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import org.schabi.newpipe.App;
import static android.content.Context.BATTERY_SERVICE;
import static android.content.Context.UI_MODE_SERVICE;
public final class AndroidTvUtils {
public final class DeviceUtils {
private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
private static Boolean isTV = null;
private AndroidTvUtils() {
private DeviceUtils() {
}
public static boolean isTv(final Context context) {
if (AndroidTvUtils.isTV != null) {
return AndroidTvUtils.isTV;
if (isTV != null) {
return isTV;
}
PackageManager pm = App.getApp().getPackageManager();
@ -48,8 +49,15 @@ public final class AndroidTvUtils {
isTv = isTv || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
AndroidTvUtils.isTV = isTv;
return AndroidTvUtils.isTV;
DeviceUtils.isTV = isTv;
return DeviceUtils.isTV;
}
public static boolean isTablet(@NonNull final Context context) {
return (context
.getResources()
.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE;
}
public static boolean isConfirmKey(final int keyCode) {

View File

@ -536,7 +536,7 @@ public final class ListHelper {
* @param context App context
* @return {@code true} if connected to a metered network
*/
private static boolean isMeteredNetwork(final Context context) {
public static boolean isMeteredNetwork(final Context context) {
ConnectivityManager manager
= (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (manager == null || manager.getActiveNetworkInfo() == null) {

View File

@ -13,6 +13,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -46,14 +47,12 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BackgroundPlayerActivity;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayerActivity;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@ -85,6 +84,7 @@ public final class NavigationHelper {
intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
}
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO);
return intent;
}
@ -112,11 +112,13 @@ public final class NavigationHelper {
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
final int repeatMode, final float playbackSpeed,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality,
final boolean resumePlayback, final boolean startPaused,
final boolean resumePlayback,
final boolean startPaused,
final boolean isMuted) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
@ -124,12 +126,42 @@ public final class NavigationHelper {
.putExtra(BasePlayer.IS_MUTED, isMuted);
}
public static void playOnMainPlayer(final Context context, final PlayQueue queue,
public static void playOnMainPlayer(
final AppCompatActivity activity,
final PlayQueue queue,
final boolean autoPlay) {
playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay);
}
public static void playOnMainPlayer(
final FragmentManager fragmentManager,
final PlayQueue queue,
final boolean autoPlay) {
final PlayQueueItem currentStream = queue.getItem();
openVideoDetailFragment(
fragmentManager,
currentStream.getServiceId(),
currentStream.getUrl(),
currentStream.getTitle(),
autoPlay,
queue);
}
public static void playOnMainPlayer(@NonNull final Context context,
@NonNull final PlayQueue queue,
@NonNull final StreamingService.LinkType linkType,
@NonNull final String url,
@NonNull final String title,
final boolean autoPlay,
final boolean resumePlayback) {
final Intent playerIntent
= getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback);
playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(playerIntent);
final Intent intent = getPlayerIntent(context, MainActivity.class, queue, resumePlayback);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_LINK_TYPE, linkType);
intent.putExtra(Constants.KEY_URL, url);
intent.putExtra(Constants.KEY_TITLE, title);
intent.putExtra(VideoDetailFragment.AUTO_PLAY, autoPlay);
context.startActivity(intent);
}
public static void playOnPopupPlayer(final Context context, final PlayQueue queue,
@ -140,16 +172,19 @@ public final class NavigationHelper {
}
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
startService(context,
getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback));
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
startService(context, intent);
}
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue,
public static void playOnBackgroundPlayer(final Context context,
final PlayQueue queue,
final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show();
startService(context,
getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback));
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
startService(context, intent);
}
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue,
@ -166,8 +201,10 @@ public final class NavigationHelper {
}
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
startService(context, getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue,
selectOnAppend, resumePlayback));
final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
startService(context, intent);
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue,
@ -175,12 +212,15 @@ public final class NavigationHelper {
enqueueOnBackgroundPlayer(context, queue, false, resumePlayback);
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue,
public static void enqueueOnBackgroundPlayer(final Context context,
final PlayQueue queue,
final boolean selectOnAppend,
final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
startService(context, getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue,
selectOnAppend, resumePlayback));
final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
startService(context, intent);
}
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
@ -311,31 +351,43 @@ public final class NavigationHelper {
public static void openVideoDetailFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String title) {
openVideoDetailFragment(fragmentManager, serviceId, url, title, false);
openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null);
}
public static void openVideoDetailFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String name, final boolean autoPlay) {
Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
public static void openVideoDetailFragment(
final FragmentManager fragmentManager,
final int serviceId,
final String url,
final String title,
final boolean autoPlay,
final PlayQueue playQueue) {
final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
VideoDetailFragment detailFragment = (VideoDetailFragment) fragment;
expandMainPlayer(fragment.requireActivity());
final VideoDetailFragment detailFragment = (VideoDetailFragment) fragment;
detailFragment.setAutoplay(autoPlay);
detailFragment.selectAndLoadVideo(serviceId, url, name == null ? "" : name);
detailFragment
.selectAndLoadVideo(serviceId, url, title == null ? "" : title, playQueue);
detailFragment.scrollToTop();
return;
}
VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url,
name == null ? "" : name);
final VideoDetailFragment instance = VideoDetailFragment
.getInstance(serviceId, url, title == null ? "" : title, playQueue);
instance.setAutoplay(autoPlay);
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, instance)
.addToBackStack(null)
.replace(R.id.fragment_player_holder, instance)
.runOnCommit(() -> expandMainPlayer(instance.requireActivity()))
.commit();
}
public static void expandMainPlayer(final Context context) {
final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER);
context.sendBroadcast(intent);
}
public static void openChannelFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String name) {
@ -505,10 +557,6 @@ public final class NavigationHelper {
return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class);
}
public static Intent getPopupPlayerActivityIntent(final Context context) {
return getServicePlayerActivityIntent(context, PopupVideoPlayerActivity.class);
}
private static Intent getServicePlayerActivityIntent(final Context context,
final Class activityClass) {
Intent intent = new Intent(context, activityClass);

View File

@ -31,8 +31,9 @@ public final class ShareUtils {
// no browser set as default
openInDefaultApp(context, url);
} else {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setPackage(defaultBrowserPackageName);
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setPackage(defaultBrowserPackageName)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
@ -48,7 +49,8 @@ public final class ShareUtils {
private static void openInDefaultApp(final Context context, final String url) {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
context.startActivity(Intent.createChooser(
intent, context.getString(R.string.share_dialog_title)));
intent, context.getString(R.string.share_dialog_title))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/**
@ -60,7 +62,8 @@ public final class ShareUtils {
* @return the package name of the default browser, or "android" if there's no default
*/
private static String getDefaultBrowserPackageName(final Context context) {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(
intent, PackageManager.MATCH_DEFAULT_ONLY);
return resolveInfo.activityInfo.packageName;
@ -79,7 +82,8 @@ public final class ShareUtils {
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, url);
context.startActivity(Intent.createChooser(
intent, context.getString(R.string.share_dialog_title)));
intent, context.getString(R.string.share_dialog_title))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/**

View File

@ -0,0 +1,114 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceView;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
public class ExpandableSurfaceView extends SurfaceView {
private int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
private int baseHeight = 0;
private int maxHeight = 0;
private float videoAspectRatio = 0.0f;
private float scaleX = 1.0f;
private float scaleY = 1.0f;
public ExpandableSurfaceView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (videoAspectRatio == 0.0f) {
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
final boolean verticalVideo = videoAspectRatio < 1;
// Use maxHeight only on non-fit resize mode and in vertical videos
int height = maxHeight != 0
&& resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT
&& verticalVideo ? maxHeight : baseHeight;
if (height == 0) {
return;
}
final float viewAspectRatio = width / ((float) height);
final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
scaleX = 1.0f;
scaleY = 1.0f;
switch (resizeMode) {
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
}
break;
case RESIZE_MODE_ZOOM:
if (aspectDeformation < 0) {
scaleY = viewAspectRatio / videoAspectRatio;
} else {
scaleX = videoAspectRatio / viewAspectRatio;
}
break;
default:
break;
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
/**
* Scale view only in {@link #onLayout} to make transition for ZOOM mode as smooth as possible.
*/
@Override
protected void onLayout(final boolean changed,
final int left, final int top, final int right, final int bottom) {
setScaleX(scaleX);
setScaleY(scaleY);
}
/**
* @param base The height that will be used in every resize mode as a minimum height
* @param max The max height for vertical videos in non-FIT resize modes
*/
public void setHeights(final int base, final int max) {
if (baseHeight == base && maxHeight == max) {
return;
}
baseHeight = base;
maxHeight = max;
requestLayout();
}
public void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int newResizeMode) {
if (resizeMode == newResizeMode) {
return;
}
resizeMode = newResizeMode;
requestLayout();
}
@AspectRatioFrameLayout.ResizeMode
public int getResizeMode() {
return resizeMode;
}
public void setAspectRatio(final float aspectRatio) {
if (videoAspectRatio == aspectRatio) {
return;
}
videoAspectRatio = aspectRatio;
requestLayout();
}
}

View File

@ -26,7 +26,7 @@ import android.widget.SeekBar;
import androidx.appcompat.widget.AppCompatSeekBar;
import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.DeviceUtils;
/**
* SeekBar, adapted for directional navigation. It emulates touch-related callbacks
@ -60,7 +60,7 @@ public final class FocusAwareSeekBar extends AppCompatSeekBar {
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) {
if (!isInTouchMode() && DeviceUtils.isConfirmKey(keyCode)) {
releaseTrack();
}

View File

@ -38,6 +38,7 @@ import android.view.ViewTreeObserver;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.view.WindowCallbackWrapper;
@ -95,7 +96,9 @@ public final class FocusOverlayView extends Drawable implements
if (focusedView != null && isShown(focusedView)) {
focusedView.getGlobalVisibleRect(focusRect);
} else {
}
if (shouldClearFocusRect(focusedView, focusRect)) {
focusRect.setEmpty();
}
@ -170,6 +173,16 @@ public final class FocusOverlayView extends Drawable implements
public void setColorFilter(final ColorFilter colorFilter) {
}
/*
* When any view in the player looses it's focus (after setVisibility(GONE)) the focus gets
* added to the whole fragment which has a width and height equal to the window frame.
* The easiest way to avoid the unneeded frame is to skip highlighting of rect that is
* equal to the overlayView bounds
* */
private boolean shouldClearFocusRect(@Nullable final View focusedView, final Rect focusedRect) {
return focusedView == null || focusedRect.equals(getBounds());
}
public static void setupFocusObserver(final Dialog dialog) {
Rect displayRect = new Rect();

View File

@ -89,7 +89,7 @@ public abstract class Postprocessing implements Serializable {
}
public void setTemporalDir(@NonNull File directory) {
long rnd = (int) (Math.random() * 100000f);
long rnd = (int) (Math.random() * 100000.0f);
tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp");
}

View File

@ -210,7 +210,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
} else {
h.progress.setMarquee(false);
h.status.setText("100%");
h.progress.setProgress(1f);
h.progress.setProgress(1.0f);
h.size.setText(Utility.formatBytes(item.mission.length));
}
}
@ -243,7 +243,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
double progress;
if (mission.unknownLength) {
progress = Double.NaN;
h.progress.setProgress(0f);
h.progress.setProgress(0.0f);
} else {
progress = done / length;
}
@ -310,7 +310,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
for (int i = 0; i < h.lastSpeed.length; i++) {
averageSpeed += h.lastSpeed[i];
}
averageSpeed /= h.lastSpeed.length + 1f;
averageSpeed /= h.lastSpeed.length + 1.0f;
}
String speedStr = Utility.formatSpeed(averageSpeed);

View File

@ -26,7 +26,7 @@ public class ProgressDrawable extends Drawable {
public ProgressDrawable() {
mMarqueeLine = null;// marquee disabled
mMarqueeProgress = 0f;
mMarqueeProgress = 0.0f;
mMarqueeSize = 0;
mMarqueeNext = 0;
}
@ -122,7 +122,7 @@ public class ProgressDrawable extends Drawable {
}
private void setupMarquee(int width, int height) {
mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width
mMarqueeSize = (int) ((width * 10.0f) / 100.0f);// the size is 10% of the width
mMarqueeLine.rewind();
mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize);

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#ffffff">
<!-- Tint here is for preventing pixelization -->
<path android:fillColor="#ffffff"
android:pathData="M5,19 L15,12 5,5ZM16,5v14h3v-14z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#ffffff">
<!-- Tint here is for preventing pixelization -->
<path android:fillColor="#ffffff"
android:pathData="m19,5 l-10,7 10,7zM8,19v-14H5v14z"/>
</vector>

View File

@ -185,7 +185,7 @@
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_backward"
android:layout_width="wrap_content"
android:layout_height="35dp"
@ -198,8 +198,8 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_previous"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
@ -238,7 +238,7 @@
app:srcCompat="@drawable/ic_shuffle_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"
android:layout_width="wrap_content"
android:layout_height="35dp"
@ -251,8 +251,8 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_next"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp"
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
</RelativeLayout>

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/windowBackground">
<LinearLayout
android:id="@+id/video_item_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -42,6 +47,7 @@
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:descendantFocusability="afterDescendants"
app:layout_collapseMode="parallax">
<ImageView
@ -146,6 +152,14 @@
tools:progress="40"
tools:visibility="visible" />
<!-- Player will be inserted here in realtime -->
<FrameLayout
android:id="@+id/player_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="afterDescendants"
/>
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
@ -155,7 +169,6 @@
android:id="@+id/detail_content_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
app:layout_scrollFlags="scroll">
<!-- TITLE -->
@ -555,25 +568,22 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom|center"
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:background="@color/transparent_background_color"
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp">
</com.google.android.material.tabs.TabLayout>
</androidx.viewpager.widget.ViewPager>
</org.schabi.newpipe.views.FocusAwareCoordinator>
@ -586,3 +596,105 @@
</FrameLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/overlay_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.9"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:descendantFocusability="blocksDescendants"
android:background="?attr/windowBackground" >
<ImageButton
android:id="@+id/overlay_thumbnail"
android:layout_width="50dp"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:scaleType="fitCenter"
android:gravity="center_vertical"
android:contentDescription="@string/list_thumbnail_view_description"
android:src="@drawable/dummy_thumbnail"
android:background="@color/transparent_background_color"/>
<LinearLayout
android:id="@+id/overlay_metadata_layout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:clickable="true"
android:focusable="true"
android:layout_toEndOf="@+id/overlay_thumbnail"
android:layout_toStartOf="@+id/overlay_buttons_layout"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/overlay_title_text_view"
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:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONVideo Title LONG very LONG"/>
<TextView
android:id="@+id/overlay_channel_text_view"
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:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
<LinearLayout
android:id="@+id/overlay_buttons_layout"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:gravity="center_vertical"
android:paddingLeft="@dimen/video_item_search_padding"
android:layout_alignParentEnd="true"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/overlay_play_pause_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:padding="10dp"
android:scaleType="center"
app:srcCompat="?attr/ic_play_arrow"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/overlay_close_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginLeft="2dp"
android:padding="10dp"
android:scaleType="center"
app:srcCompat="?attr/ic_close"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
</RelativeLayout>
</FrameLayout>

View File

@ -2,32 +2,24 @@
<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:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/black"
android:gravity="center">
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:id="@+id/aspectRatioLayout"
<org.schabi.newpipe.views.ExpandableSurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center">
android:layout_centerInParent="true"/>
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:layout_alignBottom="@+id/surfaceView"/>
<com.google.android.exoplayer2.ui.SubtitleView
android:id="@+id/subtitleView"
@ -46,85 +38,6 @@
tools:ignore="ContentDescription"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/playQueuePanel"
android:layout_width="380dp"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:visibility="invisible"
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"
app:srcCompat="@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"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<androidx.recyclerview.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"
@ -133,37 +46,72 @@
android:visibility="gone"
tools:visibility="visible">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_top_background"
android:layout_alignParentTop="true" />
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_background"
android:layout_alignParentBottom="true" />
<!-- All top controls in this layout -->
<RelativeLayout
android:id="@+id/playbackWindowRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<RelativeLayout
<LinearLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/player_controls_top_background"
android:orientation="vertical"
android:gravity="top"
android:descendantFocusability="afterDescendants"
android:paddingTop="@dimen/player_main_top_padding"
android:paddingStart="@dimen/player_main_controls_padding"
android:paddingEnd="@dimen/player_main_controls_padding"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/primaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="45dp"
android:baselineAligned="false"
android:descendantFocusability="afterDescendants"
android:gravity="top"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
tools:ignore="RtlHardcoded">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playerCloseButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="?attr/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone" />
<LinearLayout
android:id="@+id/metadataView"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/qualityTextView"
android:gravity="top"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
tools:ignore="RtlHardcoded">
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
<TextView
android:id="@+id/titleTextView"
@ -177,7 +125,6 @@
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
android:clickable="true"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONG"/>
@ -192,94 +139,86 @@
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
android:clickable="true"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
<Button
android:id="@+id/qualityTextView"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/playbackSpeed"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="50dp"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:textAllCaps="false"
android:padding="5dp"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"/>
<Button
android:id="@+id/playbackSpeed"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/queueButton"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minHeight="35dp"
android:minWidth="40dp"
android:minWidth="0dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:textAllCaps="false"
android:padding="5dp"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x" />
tools:text="1x"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="30dp"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/moreOptionsButton"
android:paddingTop="5dp"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_list_white_24dp"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="2dp"
android:padding="5dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<RelativeLayout
</LinearLayout>
<LinearLayout
android:id="@+id/secondaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/topControls"
android:gravity="top"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
<Button
android:id="@+id/resizeTextView"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_alignParentLeft="true"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="50dp"
android:textColor="@android:color/white"
@ -290,148 +229,120 @@
tools:text="FIT"/>
<Button
style="@style/Widget.AppCompat.Button.Borderless"
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_toLeftOf="@id/switchMute"
android:layout_toRightOf="@id/resizeTextView"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center|left"
android:minHeight="35dp"
android:minWidth="40dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:lines="1"
android:ellipsize="end"
android:minWidth="50dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:textAllCaps="false"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English" />
tools:text="English"/>
<ImageButton
android:id="@+id/kodi"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
<Space
android:id="@+id/spaceBeforeButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="3"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_cast_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/play_with_kodi_title"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/share"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@id/kodi"
android:layout_alignWithParentIfMissing="true"
android:layout_centerVertical="true"
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/openInBrowser"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_language_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/open_in_browser"
tools:ignore="RtlHardcoded"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_share_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/share"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/toggleOrientation"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/share"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_screen_rotation_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/toggle_orientation"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/switchPopup"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_fullscreen_exit_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_popup"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/switchBackground"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_headset_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
tools:ignore="RtlHardcoded"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchMute"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchBackground"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="37dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_volume_off_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
android:contentDescription="@string/mute"
tools:ignore="RtlHardcoded" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/bottomControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
android:layout_alignParentBottom="true"
android:background="@drawable/player_controls_background"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
android:paddingLeft="@dimen/player_main_controls_padding"
android:paddingRight="@dimen/player_main_controls_padding">
<TextView
android:id="@+id/playbackCurrentTime"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="40dp"
android:minHeight="30dp"
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText"
@ -447,6 +358,7 @@
android:paddingBottom="4dp"
android:paddingTop="8dp"
tools:progress="25"
android:nextFocusDown="@id/screenRotationButton"
tools:secondaryProgress="50"/>
<TextView
@ -472,67 +384,147 @@
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/screenRotationButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:padding="@dimen/player_main_buttons_padding"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:nextFocusUp="@id/playbackSeekBar"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>
</RelativeLayout>
<ImageButton
android:id="@+id/playPauseButton"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:weightSum="5.5">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPreviousButton"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPauseButton"
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_pause_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/playPreviousButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="30dp"
android:layout_centerInParent="true"
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
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playNextButton"
android:layout_width="50dp"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:layout_centerInParent="true"
android:layout_toEndOf="@id/playPauseButton"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_next"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription"/>
<Button
android:id="@+id/closeButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/playPauseButton"
android:layout_centerInParent="true"
android:layout_marginTop="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:text="@string/close"
android:textAllCaps="true"
android:textColor="@color/white"
android:visibility="invisible" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/playQueuePanel"
android:layout_width="380dp"
android:layout_alignParentEnd="true"
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_alignParentEnd="true"
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"/>
<androidx.appcompat.widget.AppCompatImageButton
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"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<androidx.recyclerview.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="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
@ -572,10 +564,7 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_gravity="center"
android:layout_toEndOf="@+id/loading_panel"
android:layout_toRightOf="@+id/loading_panel"
tools:ignore="RtlHardcoded">
<RelativeLayout
@ -650,4 +639,41 @@
tools:visibility="visible" />
</RelativeLayout>
<TextView
android:id="@+id/resizing_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|top"
android:background="#6e000000"
android:gravity="center"
android:padding="5dp"
android:text="@string/popup_resizing_indicator_title"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="gone" />
<View
android:id="@+id/closingOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AAFF0000"
android:visibility="gone" />
<Button
android:id="@+id/closeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:text="@string/close"
android:textAllCaps="true"
android:textColor="@color/white"
android:visibility="gone" />
</RelativeLayout>

View File

@ -100,6 +100,7 @@
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:typeface="monospace"/>
</HorizontalScrollView>

View File

@ -1,18 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<org.schabi.newpipe.views.FocusAwareDrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<FrameLayout
<org.schabi.newpipe.views.FocusAwareCoordinator
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.schabi.newpipe.MainActivity">
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_holder"
android:layout_width="match_parent"
@ -20,7 +18,18 @@
android:layout_marginTop="?attr/actionBarSize" />
<include layout="@layout/toolbar_layout" />
</FrameLayout>
<FrameLayout
android:id="@+id/fragment_player_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="org.schabi.newpipe.player.event.CustomBottomSheetBehavior">
</FrameLayout>
</org.schabi.newpipe.views.FocusAwareCoordinator>
<include layout="@layout/drawer_layout"/>

View File

@ -184,10 +184,10 @@
app:srcCompat="@drawable/ic_repeat_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_backward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_toLeftOf="@+id/control_fast_rewind"
@ -196,7 +196,7 @@
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_previous"
app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton
@ -263,10 +263,10 @@
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_fastforward"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toRightOf="@+id/control_fast_forward"
@ -275,7 +275,7 @@
android:focusable="true"
android:scaleType="fitCenter"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_next"
app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton

View File

@ -5,13 +5,13 @@
android:id="@+id/video_item_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/windowBackground"
android:focusableInTouchMode="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/detail_main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="wrap_content">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbarlayout"
@ -140,6 +140,13 @@
tools:progress="40"
tools:visibility="visible" />
<!-- Player will be inserted here in realtime -->
<FrameLayout
android:id="@+id/player_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
@ -149,7 +156,6 @@
android:id="@+id/detail_content_root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
app:layout_scrollFlags="scroll">
<!-- TITLE -->
@ -543,25 +549,122 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom|center"
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:background="@color/transparent_background_color"
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp">
</com.google.android.material.tabs.TabLayout>
</androidx.viewpager.widget.ViewPager>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<RelativeLayout
android:id="@+id/overlay_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.9"
android:background="?attr/windowBackground" >
<ImageButton
android:paddingLeft="@dimen/video_item_search_padding"
android:id="@+id/overlay_thumbnail"
android:layout_width="50dp"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:scaleType="fitCenter"
android:gravity="center_vertical"
android:contentDescription="@string/list_thumbnail_view_description"
android:src="@drawable/dummy_thumbnail"
android:background="@color/transparent_background_color"/>
<LinearLayout
android:id="@+id/overlay_metadata_layout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:clickable="true"
android:focusable="true"
android:layout_toEndOf="@+id/overlay_thumbnail"
android:layout_toStartOf="@+id/overlay_buttons_layout"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/overlay_title_text_view"
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:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONVideo Title LONG very LONG"/>
<TextView
android:id="@+id/overlay_channel_text_view"
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:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
<LinearLayout
android:id="@+id/overlay_buttons_layout"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:gravity="center_vertical"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:layout_alignParentEnd="true"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/overlay_play_pause_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:padding="10dp"
android:scaleType="center"
app:srcCompat="?attr/ic_play_arrow"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/overlay_close_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginLeft="2dp"
android:padding="10dp"
android:scaleType="center"
app:srcCompat="?attr/ic_close"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
</RelativeLayout>
</FrameLayout>

View File

@ -2,32 +2,24 @@
<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:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/black"
android:gravity="center">
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:id="@+id/aspectRatioLayout"
<org.schabi.newpipe.views.ExpandableSurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center">
android:layout_centerInParent="true"/>
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:layout_alignBottom="@+id/surfaceView"/>
<com.google.android.exoplayer2.ui.SubtitleView
android:id="@+id/subtitleView"
@ -46,83 +38,6 @@
tools:ignore="ContentDescription"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/playQueuePanel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
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"
app:srcCompat="@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"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<androidx.recyclerview.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"
@ -131,37 +46,70 @@
android:visibility="gone"
tools:visibility="visible">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_top_background"
android:layout_alignParentTop="true" />
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_background"
android:layout_alignParentBottom="true" />
<!-- All top controls in this layout -->
<RelativeLayout
android:id="@+id/playbackWindowRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<RelativeLayout
<LinearLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/player_controls_top_background"
android:orientation="vertical"
android:gravity="top"
android:paddingTop="@dimen/player_main_top_padding"
android:paddingStart="@dimen/player_main_controls_padding"
android:paddingEnd="@dimen/player_main_controls_padding"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/primaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="45dp"
android:baselineAligned="false"
android:gravity="top"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/playerCloseButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="?attr/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone" />
<LinearLayout
android:id="@+id/metadataView"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/qualityTextView"
android:gravity="top"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
tools:ignore="RtlHardcoded">
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
<TextView
android:id="@+id/titleTextView"
@ -200,70 +148,67 @@
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/playbackSpeed"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="50dp"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
android:visibility="visible"
tools:ignore="HardcodedText,RtlHardcoded"/>
<TextView
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/queueButton"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minHeight="35dp"
android:minWidth="40dp"
android:minWidth="0dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x" />
tools:text="1x"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="30dp"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/moreOptionsButton"
android:paddingTop="5dp"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_list_white_24dp"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="2dp"
android:padding="5dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<RelativeLayout
</LinearLayout>
<LinearLayout
android:id="@+id/secondaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/topControls"
android:gravity="top"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible">
@ -272,9 +217,8 @@
android:id="@+id/resizeTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_alignParentLeft="true"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="50dp"
android:textColor="@android:color/white"
@ -287,144 +231,117 @@
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_toLeftOf="@id/switchMute"
android:layout_toRightOf="@id/resizeTextView"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center|left"
android:minHeight="35dp"
android:minWidth="40dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:maxWidth="100dp"
android:lines="1"
android:ellipsize="end"
android:minWidth="50dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English" />
tools:text="English"/>
<ImageButton
android:id="@+id/kodi"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
<Space
android:id="@+id/spaceBeforeButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="3"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_cast_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/play_with_kodi_title"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/share"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@id/kodi"
android:layout_alignWithParentIfMissing="true"
android:layout_centerVertical="true"
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/openInBrowser"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_language_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/open_in_browser"
tools:ignore="RtlHardcoded"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_share_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/share"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/toggleOrientation"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/share"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_screen_rotation_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/toggle_orientation"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/switchPopup"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_fullscreen_exit_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_popup"
tools:ignore="RtlHardcoded"/>
<ImageButton
android:id="@+id/switchBackground"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_headset_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
tools:ignore="RtlHardcoded"/>
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchMute"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchBackground"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="37dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_volume_off_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
android:contentDescription="@string/mute"
tools:ignore="RtlHardcoded" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/bottomControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
android:layout_alignParentBottom="true"
android:background="@drawable/player_controls_background"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
android:paddingLeft="@dimen/player_main_controls_padding"
android:paddingRight="@dimen/player_main_controls_padding">
<TextView
android:id="@+id/playbackCurrentTime"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="40dp"
android:minHeight="30dp"
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText"
@ -465,67 +382,145 @@
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/screenRotationButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:padding="@dimen/player_main_buttons_padding"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>
</RelativeLayout>
<ImageButton
android:id="@+id/playPauseButton"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:weightSum="5.5">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPreviousButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginEnd="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPauseButton"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_pause_white_24dp"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/playPreviousButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="30dp"
android:layout_centerInParent="true"
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
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playNextButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:layout_centerInParent="true"
android:layout_toEndOf="@id/playPauseButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_next"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription"/>
<Button
android:id="@+id/closeButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/playPauseButton"
android:layout_centerInParent="true"
android:layout_marginTop="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:text="@string/close"
android:textAllCaps="true"
android:textColor="@color/white"
android:visibility="invisible" />
</LinearLayout>
</RelativeLayout>
<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_alignParentEnd="true"
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"/>
<androidx.appcompat.widget.AppCompatImageButton
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"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<androidx.recyclerview.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="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
@ -565,10 +560,7 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_gravity="center"
android:layout_toEndOf="@+id/loading_panel"
android:layout_toRightOf="@+id/loading_panel"
tools:ignore="RtlHardcoded">
<RelativeLayout
@ -643,4 +635,42 @@
tools:visibility="visible" />
</RelativeLayout>
<TextView
android:id="@+id/resizing_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|top"
android:background="#6e000000"
android:gravity="center"
android:padding="5dp"
android:text="@string/popup_resizing_indicator_title"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="gone" />
<View
android:id="@+id/closingOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AAFF0000"
android:visibility="gone" />
<Button
android:id="@+id/closeButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:text="@string/close"
android:textAllCaps="true"
android:textColor="@color/white"
android:visibility="gone" />
</RelativeLayout>

View File

@ -1,299 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:gravity="center"
tools:layout_height="84dp"
tools:layout_width="@dimen/popup_minimum_width">
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:id="@+id/aspectRatioLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<com.google.android.exoplayer2.ui.SubtitleView
android:id="@+id/subtitleView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<ImageView
android:id="@+id/endScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:visibility="gone"
tools:background="@android:color/white"
tools:ignore="ContentDescription"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#32000000"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/videoPlayPause"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_centerHorizontal="false"
android:layout_centerInParent="true"
android:visibility="gone"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<RelativeLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/player_controls_top_background"
android:paddingBottom="20dp"
android:paddingLeft="2dp"
android:paddingRight="6dp"
android:paddingTop="4dp"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:gravity="center"
android:padding="5dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1080p60"/>
<TextView
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_toRightOf="@+id/qualityTextView"
android:gravity="center"
android:padding="5dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded,RtlSymmetry"
tools:text="1.75x"/>
<RelativeLayout
android:id="@+id/extraOptionsView"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_toRightOf="@+id/playbackSpeed"
android:layout_toLeftOf="@id/fullScreenButton"
android:visibility="gone">
<TextView
android:id="@+id/resizeTextView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:minWidth="50dp"
android:padding="5dp"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT" />
<TextView
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="5dp"
android:layout_toRightOf="@id/resizeTextView"
android:gravity="center|left"
android:minWidth="40dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded,RtlSymmetry"
tools:text="English" />
</RelativeLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
</RelativeLayout>
<!--Shadow Bottom Control-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/player_controls_background"
android:orientation="horizontal"
android:paddingTop="50dp"/>
<LinearLayout
android:id="@+id/bottomControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="2dp"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<TextView
android:id="@+id/playbackCurrentTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry"
tools:text="1:06:29"/>
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
tools:progress="25"
tools:secondaryProgress="50"/>
<TextView
android:id="@+id/playbackEndTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry"
tools:text="1:23:49"/>
<TextView
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center_vertical"
android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:weightSum="5.5">
<!--tools:visibility="gone">-->
<ImageView
android:id="@+id/controlAnimationView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:srcCompat="@drawable/ic_fast_rewind_white_24dp"
android:visibility="gone"
tools:ignore="ContentDescription"
tools:visibility="visible"/>
</LinearLayout>
<TextView
android:id="@+id/currentDisplaySeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#64000000"
android:paddingBottom="5dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="5dp"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:text="1:06:29"
tools:visibility="visible"/>
<RelativeLayout
android:id="@+id/loading_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/black"
android:gravity="center"
android:padding="20dp"
tools:visibility="gone">
<ProgressBar
android:id="@+id/progressBarLoadingPanel"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</RelativeLayout>
<TextView
android:id="@+id/resizing_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|top"
android:background="#6e000000"
android:gravity="center"
android:padding="5dp"
android:text="@string/popup_resizing_indicator_title"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="gone"/>
<View
android:id="@+id/closingOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AAFF0000"
android:visibility="gone"/>
</FrameLayout>

View File

@ -1,88 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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/notificationContent"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@color/background_notification_color"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/notificationCover"
android:layout_width="64dp"
android:layout_height="64dp"
android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail"
tools:ignore="ContentDescription"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/notificationSongName"
android:layout_width="match_parent"
android:ellipsize="end"
android:layout_height="wrap_content"
android:maxLines="1"
android:textSize="14sp"
android:textColor="@color/background_title_color"
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/notificationArtist"
android:layout_width="match_parent"
android:ellipsize="end"
android:layout_height="wrap_content"
android:maxLines="1"
android:textSize="12sp"
android:textColor="@color/background_subtext_color"
tools:text="Duis posuere arcu condimentum lobortis mattis."/>
</LinearLayout>
<ImageButton
android:id="@+id/notificationRepeat"
android:layout_width="40dp"
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_repeat_all"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/notificationPlayPause"
android:layout_width="45dp"
android:layout_height="match_parent"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_pause"
tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/notificationStop"
android:layout_width="40dp"
android:layout_height="40dp"
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"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>

View File

@ -19,15 +19,6 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar"
app:titleTextAppearance="@style/Toolbar.Title">
<Spinner
android:id="@+id/toolbar_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical|left"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible"/>
<include
android:id="@+id/toolbar_search_container"
layout="@layout/toolbar_search_layout"

View File

@ -7,4 +7,9 @@
android:orderInCategory="1999"
android:title="@string/switch_to_popup"
app:showAsAction="never"/>
<item android:id="@+id/action_switch_background"
android:orderInCategory="999"
android:title="@string/switch_to_background"
app:showAsAction="never"/>
</menu>

View File

@ -1,10 +0,0 @@
<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_switch_background"
android:orderInCategory="1999"
android:title="@string/switch_to_background"
app:showAsAction="never"/>
</menu>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
android:id="@+id/menu_video_options"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/toggleOrientation"
android:icon="@drawable/ic_screen_rotation_white_24dp"
android:title="@string/toggle_orientation"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/switchPopup"
android:icon="@drawable/ic_fullscreen_exit_white_24dp"
android:title="@string/switch_to_popup"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/switchBackground"
android:icon="?attr/ic_headset"
android:title="@string/switch_to_background"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@ -227,8 +227,6 @@
<string name="trending">الشائعة</string>
<string name="top_50">أفضل 50</string>
<string name="new_and_hot">جديد وساخن</string>
<string name="title_activity_background_player">مشغل الخلفية</string>
<string name="title_activity_popup_player">المشغل المنبثق</string>
<string name="play_queue_remove">حذف</string>
<string name="play_queue_stream_detail">التفاصيل</string>
<string name="play_queue_audio_settings">الإعدادات الصوتية</string>

View File

@ -273,8 +273,6 @@
<string name="player_stream_failure">无法播放此串流</string>
<string name="player_unrecoverable_failure">发生无法恢复播放器错误</string>
<string name="player_recoverable_failure">恢复播放器错误</string>
<string name="title_activity_background_player">后台播放</string>
<string name="title_activity_popup_player">悬浮窗播放器</string>
<string name="play_queue_remove">移除</string>
<string name="play_queue_stream_detail">详情</string>
<string name="play_queue_audio_settings">音频设置</string>

View File

@ -314,8 +314,6 @@
<string name="trending">Трэнды</string>
<string name="top_50">Топ 50</string>
<string name="new_and_hot">Новае і гарачае</string>
<string name="title_activity_background_player">У фоне</string>
<string name="title_activity_popup_player">У акне</string>
<string name="play_queue_remove">Выдаліць</string>
<string name="play_queue_stream_detail">Падрабязнасці</string>
<string name="play_queue_audio_settings">Налады аўдыё</string>

View File

@ -307,8 +307,6 @@
<string name="trending">Набиращи популярност</string>
<string name="top_50">Топ 50</string>
<string name="new_and_hot">Ново и горещо</string>
<string name="title_activity_background_player">Във фонов режим</string>
<string name="title_activity_popup_player">В прозорец</string>
<string name="play_queue_remove">Премахни</string>
<string name="play_queue_stream_detail">Детайли</string>
<string name="play_queue_audio_settings">Аудио настройки</string>

View File

@ -259,8 +259,6 @@
<string name="kiosk">Quiosc</string>
<string name="trending">Tendències</string>
<string name="top_50">Els millors 50</string>
<string name="title_activity_background_player">Reproductor en rerefons</string>
<string name="title_activity_popup_player">Reproductor emergent</string>
<string name="enqueue_on_background">Afegeix a la cua de reproducció en rerefons</string>
<string name="enqueue_on_popup">Afegeix a la cua de reproducció emergent</string>
<string name="start_here_on_main">Reprodueix aquí</string>

View File

@ -188,7 +188,6 @@
<string name="theme_title">ڕووكار</string>
<string name="switch_to_main">گۆڕین بۆ سەرەکی</string>
<string name="show_original_time_ago_summary">دەقە بنچینەییەکان لە خزمەتگوزارییەکانەوە لە بابەتی پەخشەکاندا دیار دەبن</string>
<string name="title_activity_popup_player">کارپێکەری پەنجەرەی بچووک</string>
<string name="download_dialog_title">داگرتن</string>
<string name="caption_setting_title">ژێرنووسەکان</string>
<string name="invalid_url_toast">بەستەر هەڵەیە</string>
@ -601,7 +600,6 @@
<string name="title_last_played">دواین کارپێکراو</string>
<string name="could_not_setup_download_menu">ناتوانرێ لیستی داگرتن دابنرێ</string>
<string name="import_export_title">هێنانەوە/هاوردەکردن</string>
<string name="title_activity_background_player">کارپێکەری پاشبنەما</string>
<string name="app_update_notification_content_title">وەشانی نوێی داوانامە بەردەستە!</string>
<string name="playlist_thumbnail_change_success">وێنۆچکەی خشتەی کارپێکردن گۆڕدرا.</string>
<string name="import_soundcloud_instructions">هێنانەوەی پەڕەی کەسی SoundCloud بەدانانی بەستەر یاخوود ئایدی:

View File

@ -234,8 +234,6 @@ otevření ve vyskakovacím okně</string>
<string name="trending">Trendy</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Nové &amp; hot</string>
<string name="title_activity_background_player">Přehrávač na pozadí</string>
<string name="title_activity_popup_player">Přehrávač v okně</string>
<string name="play_queue_remove">Odebrat</string>
<string name="play_queue_stream_detail">Podrobnosti</string>
<string name="play_queue_audio_settings">Nastavení zvuku</string>

View File

@ -315,8 +315,6 @@
<string name="trending">Populært lige nu</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Nyt og populært</string>
<string name="title_activity_background_player">Baggrundsafspiller</string>
<string name="title_activity_popup_player">Pop op-afspiller</string>
<string name="play_queue_remove">Fjern</string>
<string name="play_queue_stream_detail">Detaljer</string>
<string name="play_queue_audio_settings">Lydindstillinger</string>

View File

@ -225,8 +225,6 @@
<string name="subscription_page_summary">Abonnement-Seite</string>
<string name="feed_page_summary">Feed-Seite</string>
<string name="channel_page_summary">Kanal-Seite</string>
<string name="title_activity_background_player">Wiedergabe im Hintergrund</string>
<string name="title_activity_popup_player">Pop-up Player</string>
<string name="play_queue_stream_detail">Details</string>
<string name="top_50">Top 50</string>
<string name="player_unrecoverable_failure">Nicht behebbarer Wiedergabefehler aufgetreten</string>

View File

@ -307,8 +307,6 @@
<string name="override_current_data">Αυτό θα παρακάμψει τις τρέχουσες ρυθμίσεις σας.</string>
<string name="import_settings">Θέλετε επίσης να εισάγετε ρυθμίσεις;</string>
<string name="kiosk">Περίπτερο</string>
<string name="title_activity_background_player">Συσκευή αναπαραγωγής Παρασκηνίου</string>
<string name="title_activity_popup_player">Συσκευή αναπαραγωγής Αναδυόμενου παραθύρου</string>
<string name="play_queue_remove">Αφαίρεση</string>
<string name="play_queue_stream_detail">Λεπτομέρειες</string>
<string name="play_queue_audio_settings">Ρυθμίσεις ήχου</string>

View File

@ -162,8 +162,6 @@
<string name="player_stream_failure">Ne povis ludi tion torenton</string>
<string name="player_unrecoverable_failure">Neatendebla eraro de ludilo okazis</string>
<string name="player_recoverable_failure">Reakiri el eraro de la ludilo</string>
<string name="title_activity_background_player">Fona ludilo</string>
<string name="title_activity_popup_player">Ŝprucfenestra ludilo</string>
<string name="play_queue_remove">Retiri</string>
<string name="play_queue_stream_detail">Detalado</string>
<string name="play_queue_audio_settings">Sonaj parametroj</string>

View File

@ -230,8 +230,6 @@
<string name="player_stream_failure">No se pudo reproducir este stream</string>
<string name="player_unrecoverable_failure">Se produjo un error irrecuperable del reproductor</string>
<string name="player_recoverable_failure">Recuperándose del error del reproductor</string>
<string name="title_activity_background_player">Reproductor en segundo plano</string>
<string name="title_activity_popup_player">Reproductor emergente</string>
<string name="play_queue_remove">Quitar</string>
<string name="play_queue_stream_detail">Detalles</string>
<string name="play_queue_audio_settings">Configuración de audio</string>

View File

@ -295,8 +295,6 @@
<string name="trending">Trendid</string>
<string name="top_50">"Top 50 "</string>
<string name="new_and_hot">Uus ja kuum</string>
<string name="title_activity_background_player">Taustapleier</string>
<string name="title_activity_popup_player">Hüpikpleier</string>
<string name="play_queue_remove">Eemalda</string>
<string name="play_queue_stream_detail">Üksikasjad</string>
<string name="play_queue_audio_settings">Heli seaded</string>

View File

@ -243,8 +243,6 @@
<string name="trending">Joerak</string>
<string name="top_50">Lehen 50ak</string>
<string name="new_and_hot">Berria eta arrakastatsua</string>
<string name="title_activity_background_player">Bigarren planoko erreproduzigailua</string>
<string name="title_activity_popup_player">Laster-leiho erreproduzigailua</string>
<string name="play_queue_remove">Kendu</string>
<string name="play_queue_stream_detail">Xehetasunak</string>
<string name="play_queue_audio_settings">Audio ezarpenak</string>

View File

@ -258,7 +258,6 @@
<string name="trending">محبوب</string>
<string name="top_50">۵۰ ویدئوی برتر</string>
<string name="new_and_hot">جدید و داغ</string>
<string name="title_activity_background_player">پخش‌کننده پس‌زمینه</string>
<string name="play_queue_remove">حذف</string>
<string name="play_queue_stream_detail">جزئیات</string>
<string name="play_queue_audio_settings">تنظیمات صدا</string>
@ -371,7 +370,6 @@
<string name="popup_playing_append">قرار دادن در صف پخش به صورت تصویر در تصویر</string>
<string name="player_unrecoverable_failure">خطای عدم احیای پخش‌کننده رخ داد</string>
<string name="player_recoverable_failure">در حال احیا از خطای پخش‌کننده</string>
<string name="title_activity_popup_player">پخش‌کننده تصویر در تصویر</string>
<string name="enqueue_on_background">در صف پخش پس‌زمینه قرار بده</string>
<string name="enqueue_on_popup">در صف پخش تصویر در تصویر قرار بده</string>
<string name="start_here_on_main">شروع پخش در اینجا</string>

View File

@ -230,8 +230,6 @@
<string name="player_stream_failure">Tätä suoratoistosisältöä ei voitu toistaa</string>
<string name="player_unrecoverable_failure">Palautuskelvoton soittimen virhe</string>
<string name="player_recoverable_failure">Palaudutaan soittimen virheestä</string>
<string name="title_activity_background_player">Taustasoitin</string>
<string name="title_activity_popup_player">Ponnahdusikkunasoitin</string>
<string name="play_queue_remove">Poista</string>
<string name="play_queue_stream_detail">Yksityiskohdat</string>
<string name="play_queue_audio_settings">Ääniasetukset</string>

View File

@ -224,8 +224,6 @@
<string name="player_stream_failure">Impossible de lire ce flux</string>
<string name="player_unrecoverable_failure">Une erreur irrécupérable du lecteur est survenue</string>
<string name="no_channel_subscribed_yet">Pas encore dabonnements de chaîne</string>
<string name="title_activity_background_player">Lecteur en arrière-plan</string>
<string name="title_activity_popup_player">Lecteur flottant</string>
<string name="play_queue_remove">Retirer</string>
<string name="play_queue_stream_detail">Détails</string>
<string name="play_queue_audio_settings">Paramètres audios</string>

View File

@ -341,8 +341,6 @@
<string name="trending">Tendencias</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Novo e popular</string>
<string name="title_activity_background_player">Reprodutor en segundo plano</string>
<string name="title_activity_popup_player">Reprodutor «popup»</string>
<string name="play_queue_remove">Eliminar</string>
<string name="play_queue_stream_detail">Detalles</string>
<string name="play_queue_audio_settings">Opcións de audio</string>

View File

@ -243,8 +243,6 @@
<string name="trending">החמים</string>
<string name="top_50">50 המובילים</string>
<string name="new_and_hot">חדש וחם</string>
<string name="title_activity_background_player">נגן רקע</string>
<string name="title_activity_popup_player">נגן צף</string>
<string name="play_queue_remove">הסרה</string>
<string name="play_queue_stream_detail">פרטים</string>
<string name="play_queue_audio_settings">אפשרויות שמע</string>

View File

@ -232,8 +232,6 @@
<string name="kiosk">kiosk</string>
<string name="top_50">टॉप 50</string>
<string name="new_and_hot">नया और गरम</string>
<string name="title_activity_background_player">पृष्ठभूमि प्लेयर</string>
<string name="title_activity_popup_player">पॉपअप प्लेयर</string>
<string name="play_queue_remove">निकाले</string>
<string name="play_queue_stream_detail">विवरण</string>
<string name="hold_to_append">जोड़ने के लिए पकड़ें रहे</string>

View File

@ -234,8 +234,6 @@
<string name="trending">U trendu</string>
<string name="top_50">Vrh 50</string>
<string name="new_and_hot">Novo i popularno</string>
<string name="title_activity_background_player">Pozadinski player</string>
<string name="title_activity_popup_player">Skočni reproduktor</string>
<string name="play_queue_remove">Ukloni</string>
<string name="play_queue_stream_detail">Detalji</string>
<string name="play_queue_audio_settings">Postavke zvuka</string>

View File

@ -296,7 +296,6 @@
<string name="trending">Felkapott</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Új és friss</string>
<string name="title_activity_popup_player">Felugró ablak lejátszó</string>
<string name="play_queue_remove">Eltávolítás</string>
<string name="play_queue_stream_detail">Részletek</string>
<string name="play_queue_audio_settings">Hang beállítások</string>

View File

@ -270,8 +270,6 @@
<string name="trending">Trending</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Baru &amp; panas</string>
<string name="title_activity_background_player">Pemutar latar belakang</string>
<string name="title_activity_popup_player">Pemutar popup</string>
<string name="play_queue_remove">Hapus</string>
<string name="play_queue_stream_detail">Detail</string>
<string name="play_queue_audio_settings">Pengaturan Audio</string>

View File

@ -231,8 +231,6 @@
<string name="player_stream_failure">Impossibile riprodurre questo flusso</string>
<string name="player_unrecoverable_failure">Si è verificato un errore irreversibile</string>
<string name="player_recoverable_failure">Ripristino dell\'errore del lettore multimediale</string>
<string name="title_activity_background_player">Riproduzione in Sottofondo</string>
<string name="title_activity_popup_player">Lettore Popup</string>
<string name="play_queue_remove">Rimuovi</string>
<string name="play_queue_stream_detail">Dettagli</string>
<string name="play_queue_audio_settings">Impostazioni Audio</string>

View File

@ -216,7 +216,6 @@
<string name="kiosk">Kiosk</string>
<string name="trending">人気</string>
<string name="top_50">トップ50</string>
<string name="title_activity_popup_player">ポップアップ再生</string>
<string name="play_queue_remove">削除</string>
<string name="play_queue_stream_detail">詳細</string>
<string name="play_queue_audio_settings">音声の設定</string>
@ -241,7 +240,6 @@
<string name="no_valid_zip_file">有効な ZIP ファイルではありません</string>
<string name="could_not_import_all_files">警告: すべてのファイルをインポートできませんでした。</string>
<string name="override_current_data">これにより、現在の設定が上書きされます。</string>
<string name="title_activity_background_player">バックグラウンド再生</string>
<string name="start_here_on_main">ここから再生を開始</string>
<string name="start_here_on_background">バックグラウンドで連続再生を開始</string>
<string name="drawer_open">ドロワーを開く</string>

View File

@ -230,8 +230,6 @@
<string name="trending">인기 급상승</string>
<string name="top_50">탑 50</string>
<string name="new_and_hot">신작 &amp; 뜨는 동영상</string>
<string name="title_activity_background_player">백그라운드 플레이어</string>
<string name="title_activity_popup_player">팝업 플레이어</string>
<string name="play_queue_remove">제거</string>
<string name="play_queue_stream_detail">상세 정보</string>
<string name="play_queue_audio_settings">오디오 설정</string>

View File

@ -252,7 +252,6 @@
<string name="title_last_played">دواین کارپێکراو</string>
<string name="title_most_played">زۆرترین کارپێکردن</string>
<string name="main_page_content">ناوەڕۆکی پەڕەی سەرەکی</string>
<string name="title_activity_popup_player">کارپێکەری پەنجەرەی بچووک</string>
<string name="play_queue_remove">لادان</string>
<string name="play_queue_stream_detail">وردەکارییەکان</string>
<string name="play_queue_audio_settings">ڕێکخستنەکانی دەنگ</string>
@ -434,7 +433,6 @@
<string name="trending">پڕبینەرەکان</string>
<string name="top_50">باشترین 50</string>
<string name="new_and_hot">نوێ &amp; چالاک</string>
<string name="title_activity_background_player">کارپێکەری پاشبنەما</string>
<string name="hold_to_append">پەنجەت داگرە بۆ ڕیزنەبوون</string>
<string name="enqueue_on_background">ڕیزنەبوون لە پاشبنەما</string>
<string name="enqueue_on_popup">ڕیزنەبوون لە پەنجەرەی بچووک</string>

View File

@ -267,8 +267,6 @@
<string name="trending">Tendencijos</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Nauja ir karšta</string>
<string name="title_activity_background_player">Foninis grotuvas</string>
<string name="title_activity_popup_player">Langelio rėžimo grotuvas</string>
<string name="play_queue_remove">Pašalinti</string>
<string name="play_queue_stream_detail">Detalės</string>
<string name="play_queue_audio_settings">Garso nustatymai</string>

View File

@ -290,8 +290,6 @@
<string name="trending">Популарни</string>
<string name="top_50">Топ 50</string>
<string name="new_and_hot">Нови и жешки</string>
<string name="title_activity_background_player">Позадински плеер</string>
<string name="title_activity_popup_player">Подпрозорче</string>
<string name="play_queue_remove">Одстрани</string>
<string name="play_queue_stream_detail">Детали</string>
<string name="play_queue_audio_settings">Звучни поставки</string>

View File

@ -79,8 +79,6 @@
<string name="play_queue_audio_settings">ഓഡിയോ ക്രമീകരണങ്ങൾ</string>
<string name="play_queue_stream_detail">വിശദാംശങ്ങൾ</string>
<string name="play_queue_remove">നീക്കം ചെയ്യുക</string>
<string name="title_activity_popup_player">പോപ്-അപ് പ്ലെയർ</string>
<string name="title_activity_background_player">ബാക്ക്ഗ്രൗണ്ട് പ്ലേയർ</string>
<string name="conferences">സമ്മേളനങ്ങൾ</string>
<string name="most_liked">ഏറ്റവും ഇഷ്ടപ്പെട്ടത്</string>
<string name="recently_added">സമീപകാലത്ത് ചേർത്തത്</string>

View File

@ -328,8 +328,6 @@
<string name="top_50">Top 50</string>
<string name="new_and_hot">Baru &amp; panas</string>
<string name="conferences">Persidangan</string>
<string name="title_activity_background_player">Pemain latar belakang</string>
<string name="title_activity_popup_player">Pemain popup</string>
<string name="play_queue_remove">Hapuskan</string>
<string name="play_queue_stream_detail">Butiran</string>
<string name="play_queue_audio_settings">Tetapan Audio</string>

View File

@ -222,8 +222,6 @@
<string name="kiosk">Kiosk</string>
<string name="top_50">Topp 50</string>
<string name="new_and_hot">Nytt og hett</string>
<string name="title_activity_background_player">Bakgrunnsavspiller</string>
<string name="title_activity_popup_player">Oppsprettsavspiller</string>
<string name="play_queue_remove">Fjern</string>
<string name="play_queue_stream_detail">Detaljer</string>
<string name="play_queue_audio_settings">Lydinnstillinger</string>

View File

@ -334,8 +334,6 @@
<string name="top_50">शीर्ष 50</string>
<string name="new_and_hot">नयाँ र तात्तातो</string>
<string name="conferences">सम्मेलनहरु</string>
<string name="title_activity_background_player">प्ले लाममा</string>
<string name="title_activity_popup_player">पपअप प्लयेर</string>
<string name="play_queue_remove">हटाउ</string>
<string name="play_queue_stream_detail">विवरण</string>
<string name="play_queue_audio_settings">अडियो सेटिङहरू</string>

View File

@ -291,8 +291,6 @@
<string name="trending">Populair</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Nieuw en populair</string>
<string name="title_activity_background_player">Achtergrondspeler</string>
<string name="title_activity_popup_player">Pop-upspeler</string>
<string name="play_queue_remove">Verwijderen</string>
<string name="play_queue_stream_detail">Details</string>
<string name="play_queue_audio_settings">Audio-instellingen</string>

View File

@ -231,8 +231,6 @@
<string name="trending">Populair</string>
<string name="top_50">Top 50</string>
<string name="new_and_hot">Nieuw en populair</string>
<string name="title_activity_background_player">Achtergrondspeler</string>
<string name="title_activity_popup_player">Pop-upspeler</string>
<string name="play_queue_remove">Verwijderen</string>
<string name="play_queue_stream_detail">Details</string>
<string name="play_queue_audio_settings">Audio-instellingen</string>

View File

@ -299,8 +299,6 @@
<string name="trending">ਰੁਝਾਨ ਵਿੱਚ</string>
<string name="top_50">ਟੌਪ 50</string>
<string name="new_and_hot">ਨਵਾਂ ਅਤੇ ਗਰਮਾ-ਗਰਮ</string>
<string name="title_activity_background_player">ਬੈਕਗ੍ਰਾਉਂਡ ਪਲੇਅਰ</string>
<string name="title_activity_popup_player">ਪੌਪ-ਅਪ ਪਲੇਅਰ</string>
<string name="play_queue_remove">ਹਟਾਓ</string>
<string name="play_queue_stream_detail">ਵੇਰਵੇ</string>
<string name="play_queue_audio_settings">ਆਡੀਓ ਸੈਟਿੰਗਾਂ</string>

Some files were not shown because too many files have changed in this diff Show More